@mintplayer/ng-spark 22.22.0 → 22.24.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 (68) hide show
  1. package/README.md +181 -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 +308 -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-grid.mjs +2259 -1117
  9. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  10. package/fesm2022/mintplayer-ng-spark-history.mjs +611 -0
  11. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -0
  12. package/fesm2022/mintplayer-ng-spark-icon.mjs +64 -27
  13. package/fesm2022/mintplayer-ng-spark-icon.mjs.map +1 -1
  14. package/fesm2022/mintplayer-ng-spark-models.mjs +365 -753
  15. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-moderation.mjs +1212 -0
  17. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -0
  18. package/fesm2022/mintplayer-ng-spark-panels.mjs +52 -0
  19. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -0
  20. package/fesm2022/mintplayer-ng-spark-pipes.mjs +1122 -765
  21. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  22. package/fesm2022/mintplayer-ng-spark-po-create.mjs +273 -169
  23. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +786 -410
  25. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +292 -284
  27. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-po-form.mjs +2187 -1225
  29. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-query-list.mjs +516 -303
  31. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-renderers.mjs +17 -55
  33. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +216 -177
  35. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark-routes.mjs +24 -28
  37. package/fesm2022/mintplayer-ng-spark-routes.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-services.mjs +625 -640
  39. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-shell.mjs +1013 -397
  41. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +294 -0
  43. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -0
  44. package/fesm2022/mintplayer-ng-spark.mjs +13 -50
  45. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  46. package/package.json +17 -1
  47. package/types/mintplayer-ng-spark-attribute-description.d.ts +25 -18
  48. package/types/mintplayer-ng-spark-client-operations.d.ts +133 -160
  49. package/types/mintplayer-ng-spark-column-filter.d.ts +73 -55
  50. package/types/mintplayer-ng-spark-grid.d.ts +885 -448
  51. package/types/mintplayer-ng-spark-history.d.ts +223 -0
  52. package/types/mintplayer-ng-spark-icon.d.ts +15 -13
  53. package/types/mintplayer-ng-spark-models.d.ts +624 -573
  54. package/types/mintplayer-ng-spark-moderation.d.ts +402 -0
  55. package/types/mintplayer-ng-spark-panels.d.ts +108 -0
  56. package/types/mintplayer-ng-spark-pipes.d.ts +120 -149
  57. package/types/mintplayer-ng-spark-po-create.d.ts +46 -37
  58. package/types/mintplayer-ng-spark-po-detail.d.ts +177 -117
  59. package/types/mintplayer-ng-spark-po-edit.d.ts +64 -65
  60. package/types/mintplayer-ng-spark-po-form.d.ts +507 -371
  61. package/types/mintplayer-ng-spark-query-list.d.ts +118 -82
  62. package/types/mintplayer-ng-spark-renderers.d.ts +79 -85
  63. package/types/mintplayer-ng-spark-retry-action-modal.d.ts +31 -34
  64. package/types/mintplayer-ng-spark-routes.d.ts +7 -11
  65. package/types/mintplayer-ng-spark-services.d.ts +377 -261
  66. package/types/mintplayer-ng-spark-shell.d.ts +209 -128
  67. package/types/mintplayer-ng-spark-soft-delete.d.ts +90 -0
  68. package/types/mintplayer-ng-spark.d.ts +7 -12
@@ -1,411 +1,787 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, input, output, signal, computed, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
- import * as i1 from '@angular/common';
5
- import { CommonModule, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
6
- import * as i2 from '@angular/router';
7
- import { ActivatedRoute, Router, RouterModule } from '@angular/router';
8
- import { Color } from '@mintplayer/ng-bootstrap';
9
- import { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';
10
- import { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';
11
- import { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';
12
- import { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';
13
- import { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';
14
- import { BsTableComponent } from '@mintplayer/ng-bootstrap/table';
15
- import { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } from '@mintplayer/ng-bootstrap/tab-control';
16
- import { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';
17
- import { SparkService, SparkLanguageService } from '@mintplayer/ng-spark/services';
18
- import { SparkQueryRefreshService, SparkAttributeRefreshService } from '@mintplayer/ng-spark/client-operations';
19
- import { ResolveTranslationPipe, TranslateKeyPipe, AttributeValuePipe, RawAttributeValuePipe, AsDetailColumnsPipe, AsDetailCellValuePipe, ArrayValuePipe, ReferenceLinkRoutePipe, ReferenceChipsPipe, ParsedDatePipe } from '@mintplayer/ng-spark/pipes';
20
- import { SparkIconComponent } from '@mintplayer/ng-spark/icon';
21
- import { SparkAttributeDescriptionComponent } from '@mintplayer/ng-spark/attribute-description';
22
- import { SparkQueryCardComponent, SparkGridCellComponent } from '@mintplayer/ng-spark/grid';
23
- import { SPARK_ATTRIBUTE_RENDERERS, rendererValue, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';
24
- import { hasShowedOnFlag, ShowedOn } from '@mintplayer/ng-spark/models';
25
-
26
- class SparkPoDetailComponent {
27
- route = inject(ActivatedRoute);
28
- router = inject(Router);
29
- sparkService = inject(SparkService);
30
- queryRefresh = inject(SparkQueryRefreshService);
31
- attributeRefresh = inject(SparkAttributeRefreshService);
32
- lang = inject(SparkLanguageService);
33
- rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);
34
- showCustomActions = input(true, /* @ts-ignore */
35
- ...(ngDevMode ? [{ debugName: "showCustomActions" }] : /* istanbul ignore next */ []));
36
- extraActionsTemplate = input(null, /* @ts-ignore */
37
- ...(ngDevMode ? [{ debugName: "extraActionsTemplate" }] : /* istanbul ignore next */ []));
38
- extraContentTemplate = input(null, /* @ts-ignore */
39
- ...(ngDevMode ? [{ debugName: "extraContentTemplate" }] : /* istanbul ignore next */ []));
40
- /**
41
- * Header slots forwarded to every sub-query card on this page.
42
- *
43
- * A structural directive cannot cross a component boundary, and this component is created by
44
- * the router — in a default app there is no `<spark-po-detail>` tag to project content into.
45
- * So an app that wants slotted chrome on its sub-queries substitutes its own route component
46
- * via `SparkRouteConfig.poDetail` and forwards the templates through here. Forwarding a
47
- * `TemplateRef` as an input is already this component's idiom; see the two above.
48
- *
49
- * Each slot may target one query by alias, so a single forwarded set serves a page rendering
50
- * several sub-queries.
51
- */
52
- queryIconTemplate = input(null, /* @ts-ignore */
53
- ...(ngDevMode ? [{ debugName: "queryIconTemplate" }] : /* istanbul ignore next */ []));
54
- queryCaptionTemplate = input(null, /* @ts-ignore */
55
- ...(ngDevMode ? [{ debugName: "queryCaptionTemplate" }] : /* istanbul ignore next */ []));
56
- queryActionsTemplate = input(null, /* @ts-ignore */
57
- ...(ngDevMode ? [{ debugName: "queryActionsTemplate" }] : /* istanbul ignore next */ []));
58
- edited = output();
59
- deleted = output();
60
- customActionExecuted = output();
61
- colors = Color;
62
- errorMessage = signal(null, /* @ts-ignore */
63
- ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
64
- entityType = signal(null, /* @ts-ignore */
65
- ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
66
- allEntityTypes = signal([], /* @ts-ignore */
67
- ...(ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []));
68
- item = signal(null, /* @ts-ignore */
69
- ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
70
- lookupReferenceOptions = signal({}, /* @ts-ignore */
71
- ...(ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []));
72
- asDetailTypes = signal({}, /* @ts-ignore */
73
- ...(ngDevMode ? [{ debugName: "asDetailTypes" }] : /* istanbul ignore next */ []));
74
- asDetailReferenceOptions = signal({}, /* @ts-ignore */
75
- ...(ngDevMode ? [{ debugName: "asDetailReferenceOptions" }] : /* istanbul ignore next */ []));
76
- type = '';
77
- id = '';
78
- canEdit = signal(false, /* @ts-ignore */
79
- ...(ngDevMode ? [{ debugName: "canEdit" }] : /* istanbul ignore next */ []));
80
- canDelete = signal(false, /* @ts-ignore */
81
- ...(ngDevMode ? [{ debugName: "canDelete" }] : /* istanbul ignore next */ []));
82
- customActions = signal([], /* @ts-ignore */
83
- ...(ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []));
84
- /**
85
- * The actions actually offered for the object on screen.
86
- *
87
- * The catalogue at `/spark/actions/{objectTypeId}` is per TYPE -- the server is never told which
88
- * row is open -- so an action that applies to only some rows cannot be filtered there. The
89
- * entity's actions hook decides while it has the entity in hand and withholds what does not
90
- * apply, and the object arrives carrying that answer.
91
- *
92
- * An affordance, not a permission: the endpoint stays reachable and the action handler still
93
- * refuses on its own terms. What this prevents is offering a destructive action where it cannot
94
- * possibly apply -- Coverage showed an irreversible red "Delete data" button on every repository
95
- * page, healthy ones included, and only admitted it would refuse after the confirmation prompt.
96
- */
97
- visibleCustomActions = computed(() => {
98
- const withheld = this.item()?.disabledActions;
99
- if (!withheld?.length)
100
- return this.customActions();
101
- const lowered = new Set(withheld.map(name => name.toLowerCase()));
102
- return this.customActions().filter(action => !lowered.has(action.name.toLowerCase()));
103
- }, /* @ts-ignore */
104
- ...(ngDevMode ? [{ debugName: "visibleCustomActions" }] : /* istanbul ignore next */ []));
105
- constructor() {
106
- this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => this.onParamsChange(params));
107
- // Server-issued `refreshAttribute` patches for the object this page is showing.
108
- //
109
- // Tracks the token and the entity type, never `item()`: writing the signal this effect reads
110
- // would re-run it forever, so the read-modify-write of the object happens untracked.
111
- effect(() => {
112
- const objectTypeId = this.entityType()?.id;
113
- if (!this.attributeRefresh.tokenFor(objectTypeId, this.id))
114
- return;
115
- const patches = this.attributeRefresh.patchesFor(objectTypeId, this.id);
116
- untracked(() => this.applyAttributePatches(patches));
117
- });
118
- }
119
- /**
120
- * Applies patched attribute values in place, without a re-fetch.
121
- *
122
- * The operation carries the value the server computed after its own write, so this is both
123
- * cheaper and more correct than re-reading: a re-read goes through a RavenDB index that may
124
- * still be stale from the very write that produced the patch, and would show the old value.
125
- */
126
- applyAttributePatches(patches) {
127
- const current = this.item();
128
- if (!current)
129
- return;
130
- let changed = false;
131
- const attributes = current.attributes.map(attribute => {
132
- if (!Object.prototype.hasOwnProperty.call(patches, attribute.name))
133
- return attribute;
134
- const patch = patches[attribute.name];
135
- const next = { ...attribute };
136
- let patched = false;
137
- // Each field is applied on its own, because they are not interchangeable: an AsDetail
138
- // attribute carries its rows in object/objects and leaves value null, so applying only
139
- // value could never refresh a detail grid.
140
- //
141
- // Compared by identity in every case. A scalar skips when unchanged, and a null skips
142
- // against a null -- which matters because the server writes object/objects as null on a
143
- // scalar patch, and treating that as a change would repaint on every patch. A rewritten
144
- // collection is a new array and so never matches, which is the behaviour wanted: comparing
145
- // rows deeply would cost more than the repaint it saves.
146
- if (patch.value !== undefined && next.value !== patch.value) {
147
- next.value = patch.value;
148
- patched = true;
149
- }
150
- // Only for the attributes that actually use them. The server writes object/objects as
151
- // null on every scalar patch, so applying them unconditionally would mark a scalar
152
- // attribute changed on each patch and repaint for nothing.
153
- if (attribute.dataType === 'AsDetail') {
154
- if (patch.object !== undefined && next.object !== patch.object) {
155
- next.object = patch.object;
156
- patched = true;
157
- }
158
- if (patch.objects !== undefined && next.objects !== patch.objects) {
159
- next.objects = patch.objects;
160
- patched = true;
161
- }
162
- }
163
- if (!patched)
164
- return attribute;
165
- changed = true;
166
- return next;
167
- });
168
- if (changed)
169
- this.item.set({ ...current, attributes });
170
- }
171
- async onParamsChange(params) {
172
- this.type = params.get('type') || '';
173
- this.id = params.get('id') || '';
174
- try {
175
- const [entityTypes, item] = await Promise.all([
176
- this.sparkService.getEntityTypes(),
177
- this.sparkService.get(this.type, this.id)
178
- ]);
179
- this.allEntityTypes.set(entityTypes);
180
- this.entityType.set(entityTypes.find(t => t.id === this.type || t.alias === this.type) || null);
181
- this.item.set(item);
182
- this.loadLookupReferenceOptions();
183
- this.loadAsDetailTypes();
184
- const et = this.entityType();
185
- if (et) {
186
- const [permissions, actions] = await Promise.all([
187
- this.sparkService.getPermissions(et.id),
188
- this.sparkService.getCustomActions(et.id)
189
- ]);
190
- // Prefer the per-row affordances (#236 G5) when the server attached them — a row the
191
- // caller may read but not mutate then hides its Edit/Delete buttons instead of rendering
192
- // ones that 404. `can` is absent for types with no row rule, so fall back to type-level.
193
- // The override (not an intersection) is safe because the server computes `can` as the
194
- // intersection of type-level rights and the row rule (#243) — it never claims more than
195
- // `permissions` does.
196
- const can = item.can;
197
- this.canEdit.set(can ? can.edit : permissions.canEdit);
198
- this.canDelete.set(can ? can.delete : permissions.canDelete);
199
- this.customActions.set(actions.filter(a => a.showedOn === 'detail' || a.showedOn === 'both'));
200
- }
201
- }
202
- catch (e) {
203
- const error = e;
204
- this.errorMessage.set(error.error?.error || error.message || 'An unexpected error occurred');
205
- }
206
- }
207
- visibleAttributes = computed(() => {
208
- return this.entityType()?.attributes
209
- .filter(a => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))
210
- .sort((a, b) => a.order - b.order) || [];
211
- }, /* @ts-ignore */
212
- ...(ngDevMode ? [{ debugName: "visibleAttributes" }] : /* istanbul ignore next */ []));
213
- static DEFAULT_TAB = { id: '__default__', name: 'Algemeen', label: { nl: 'Algemeen', en: 'General' }, order: 0 };
214
- ungroupedAttributes = computed(() => {
215
- const attrs = this.visibleAttributes();
216
- const groupIds = new Set((this.entityType()?.groups || []).map(g => g.id));
217
- return attrs.filter(a => !a.group || !groupIds.has(a.group));
218
- }, /* @ts-ignore */
219
- ...(ngDevMode ? [{ debugName: "ungroupedAttributes" }] : /* istanbul ignore next */ []));
220
- resolvedTabs = computed(() => {
221
- const et = this.entityType();
222
- const definedTabs = et?.tabs?.length ? [...et.tabs].sort((a, b) => a.order - b.order) : [];
223
- const hasUngroupedAttrs = this.ungroupedAttributes().length > 0;
224
- const hasUntabbedGroups = (et?.groups || []).some(g => !g.tab);
225
- if (hasUngroupedAttrs || hasUntabbedGroups || definedTabs.length === 0) {
226
- return [SparkPoDetailComponent.DEFAULT_TAB, ...definedTabs];
227
- }
228
- return definedTabs;
229
- }, /* @ts-ignore */
230
- ...(ngDevMode ? [{ debugName: "resolvedTabs" }] : /* istanbul ignore next */ []));
231
- groupsForTab(tab) {
232
- const groups = this.entityType()?.groups || [];
233
- if (tab.id === '__default__') {
234
- return groups.filter(g => !g.tab).sort((a, b) => a.order - b.order);
235
- }
236
- return groups.filter(g => g.tab === tab.id).sort((a, b) => a.order - b.order);
237
- }
238
- attrsForGroup(group) {
239
- return this.visibleAttributes().filter(a => a.group === group.id);
240
- }
241
- getDetailRendererComponent(attr) {
242
- if (!attr.renderer)
243
- return null;
244
- return this.rendererRegistry.find(r => r.name === attr.renderer)?.detailComponent ?? null;
245
- }
246
- getDetailRendererInputs(component, attr, item) {
247
- const itemAttr = item.attributes.find(a => a.name === attr.name);
248
- const formData = {};
249
- for (const a of item.attributes) {
250
- formData[a.name] = rendererValue(a);
251
- }
252
- return withDeclaredInputs(component, {
253
- value: rendererValue(itemAttr),
254
- attribute: attr,
255
- options: attr.rendererOptions,
256
- formData,
257
- item,
258
- });
259
- }
260
- /** Column renderer for a cell of an AsDetail sub-table (so embedded rows honor `col.renderer` too). */
261
- getAsDetailCellRendererComponent(col) {
262
- if (!col.renderer)
263
- return null;
264
- return this.rendererRegistry.find(r => r.name === col.renderer)?.columnComponent ?? null;
265
- }
266
- getAsDetailCellRendererInputs(component, row, col) {
267
- return withDeclaredInputs(component, {
268
- value: row[col.name],
269
- attribute: col,
270
- options: col.rendererOptions,
271
- item: row,
272
- });
273
- }
274
- async loadLookupReferenceOptions() {
275
- const lookupAttrs = this.visibleAttributes().filter(a => a.lookupReferenceType);
276
- if (lookupAttrs.length === 0)
277
- return;
278
- const lookupNames = [...new Set(lookupAttrs.map(a => a.lookupReferenceType))];
279
- // A lookup that fails to load only costs its labels: the attribute then shows its raw value
280
- // (attributeValue's fallback) instead of the whole page erroring over a secondary request (#453).
281
- const entries = await Promise.all(lookupNames.map(async (name) => {
282
- const result = await this.sparkService.getLookupReference(name).catch(() => null);
283
- return [name, result];
284
- }));
285
- this.lookupReferenceOptions.set(entries.reduce((acc, [k, v]) => v ? { ...acc, [k]: v } : acc, {}));
286
- }
287
- async loadAsDetailTypes() {
288
- const asDetailAttrs = this.visibleAttributes().filter(a => a.dataType === 'AsDetail' && a.isArray && a.asDetailType);
289
- if (asDetailAttrs.length === 0)
290
- return;
291
- const types = this.allEntityTypes();
292
- const newAsDetailTypes = {};
293
- // Same fallback as the form: a row type is usually absent from the Query-gated catalogue, and
294
- // the server now ships its definition alongside the parent (#385).
295
- const embedded = this.entityType()?.detailTypes ?? [];
296
- for (const attr of asDetailAttrs) {
297
- const asDetailType = types.find(t => t.clrType === attr.asDetailType)
298
- ?? embedded.find(t => t.clrType === attr.asDetailType);
299
- if (asDetailType) {
300
- newAsDetailTypes[attr.name] = asDetailType;
301
- const refCols = asDetailType.attributes.filter(a => a.dataType === 'Reference' && a.query);
302
- if (refCols.length > 0) {
303
- const refEntries = await Promise.all(refCols.map(async (col) => {
304
- const result = await this.sparkService.executeQueryByName(col.query, {
305
- parentId: this.id,
306
- parentType: this.type,
307
- });
308
- return [col.name, result.items];
309
- }));
310
- this.asDetailReferenceOptions.update(prev => ({
311
- ...prev,
312
- [attr.name]: refEntries.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
313
- }));
314
- }
315
- }
316
- }
317
- this.asDetailTypes.set(newAsDetailTypes);
318
- }
319
- /**
320
- * The bootstrap classes for a custom action's button. Outline-primary stays the default, so
321
- * every existing action looks exactly as it did; a server-declared variant opts one action into
322
- * a louder style. Restricted to a known set rather than interpolated, because the value arrives
323
- * from a JSON file and must not be able to put arbitrary classes on the button.
324
- */
325
- customActionClass(action) {
326
- const variant = action.variant?.toLowerCase();
327
- switch (variant) {
328
- case 'danger':
329
- case 'warning':
330
- case 'primary':
331
- case 'secondary':
332
- case 'success':
333
- return `btn btn-${variant}`;
334
- default:
335
- return 'btn btn-outline-primary';
336
- }
337
- }
338
- /**
339
- * The action currently running, or null. Drives the disabled state on every custom-action
340
- * button.
341
- *
342
- * A custom action is not necessarily quick: Coverage's Resync makes paged GitHub App calls for
343
- * every account the caller manages, inside the request. Without this the button stayed live and
344
- * looked inert for several seconds, and a second click queued a second full run -- which is how
345
- * a slow action and a broken one became indistinguishable.
346
- */
347
- runningAction = signal(null, /* @ts-ignore */
348
- ...(ngDevMode ? [{ debugName: "runningAction" }] : /* istanbul ignore next */ []));
349
- async onCustomAction(action) {
350
- // Guard re-entry as well as disabling the button: the template is not the only caller, and a
351
- // host component driving this method directly would otherwise bypass the check.
352
- if (this.runningAction())
353
- return;
354
- if (action.confirmationMessageKey) {
355
- const message = this.lang.t(action.confirmationMessageKey) || 'Are you sure?';
356
- if (!confirm(message))
357
- return;
358
- }
359
- this.runningAction.set(action.name);
360
- try {
361
- await this.sparkService.executeCustomAction(this.type, action.name, this.item() || undefined);
362
- this.customActionExecuted.emit({ action, item: this.item() });
363
- if (action.refreshOnCompleted) {
364
- const item = await this.sparkService.get(this.type, this.id);
365
- this.item.set(item);
366
- // The sub-query grids below do not depend on item(), so re-fetching the PO left them
367
- // showing pre-action rows -- the action appeared to have done nothing to the very
368
- // lists it changed.
369
- for (const queryAlias of (this.entityType()?.queries ?? [])) {
370
- this.queryRefresh.request(queryAlias);
371
- }
372
- }
373
- }
374
- catch (e) {
375
- const err = e;
376
- this.errorMessage.set(err.error?.error || err.message || 'Action failed');
377
- }
378
- finally {
379
- // finally, not after the try: an action that fails must not leave every button on the page
380
- // permanently disabled.
381
- this.runningAction.set(null);
382
- }
383
- }
384
- onEdit() {
385
- this.edited.emit();
386
- this.router.navigate(['/po', this.type, this.id, 'edit']);
387
- }
388
- async onDelete() {
389
- if (confirm(this.lang.t('confirmDelete'))) {
390
- await this.sparkService.delete(this.type, this.id);
391
- this.deleted.emit();
392
- this.router.navigate(['/']);
393
- }
394
- }
395
- onBack() {
396
- window.history.back();
397
- }
398
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkPoDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
399
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkPoDetailComponent, isStandalone: true, selector: "spark-po-detail", inputs: { showCustomActions: { classPropertyName: "showCustomActions", publicName: "showCustomActions", isSignal: true, isRequired: false, transformFunction: null }, extraActionsTemplate: { classPropertyName: "extraActionsTemplate", publicName: "extraActionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, extraContentTemplate: { classPropertyName: "extraContentTemplate", publicName: "extraContentTemplate", isSignal: true, isRequired: false, transformFunction: null }, queryIconTemplate: { classPropertyName: "queryIconTemplate", publicName: "queryIconTemplate", isSignal: true, isRequired: false, transformFunction: null }, queryCaptionTemplate: { classPropertyName: "queryCaptionTemplate", publicName: "queryCaptionTemplate", isSignal: true, isRequired: false, transformFunction: null }, queryActionsTemplate: { classPropertyName: "queryActionsTemplate", publicName: "queryActionsTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { edited: "edited", deleted: "deleted", customActionExecuted: "customActionExecuted" }, ngImport: i0, template: "<bs-container>\n<div class=\"container\">\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n } @else if (item(); as currentItem) {\n @if (entityType(); as et) {\n <div class=\"spark-actionbar py-2\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <button *bsPriorityNavItem=\"1\" class=\"btn btn-outline-secondary\" (click)=\"onBack()\">\n <spark-icon name=\"arrow-left\" /> {{ 'common.back' | t }}\n </button>\n @if (canEdit()) {\n <button *bsPriorityNavItem=\"2\" class=\"btn btn-primary\" (click)=\"onEdit()\">\n <spark-icon name=\"pencil\" /> {{ 'common.edit' | t }}\n </button>\n }\n @if (canDelete()) {\n <button *bsPriorityNavItem=\"3\" class=\"btn btn-danger\" (click)=\"onDelete()\">\n <spark-icon name=\"trash\" /> {{ 'common.delete' | t }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of visibleCustomActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [disabled]=\"runningAction() !== null\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n </bs-priority-nav>\n </div>\n <h2>{{ currentItem.breadcrumb || currentItem.name }}</h2>\n\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"detailTabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #detailTabContent let-tab>\n @if (tab.id === '__default__') {\n @let ungroupedAttrs = ungroupedAttributes();\n @if (ungroupedAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of ungroupedAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #detailAttrField let-attr let-currentItem=\"item\">\n <dt [sm]=\"3\">{{ (attr.label | resolveTranslation) || attr.name }}<spark-attribute-description [description]=\"attr.description\" /></dt>\n <dd [sm]=\"9\">\n @if (getDetailRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getDetailRendererInputs(rendererType, attr, currentItem)\"></ng-container>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n <!--\n Shared with the query grid. This cell used to hand-roll the renderer\n dispatch and print everything else as text, so a `boolean` column read\n \"true\" here and rendered a checkbox in a grid, and a `color` showed its\n hex. Only the VALUE is resolved locally: these rows are plain\n dictionaries, not PersistentObjects.\n\n `chips` is left unset for a reference array on purpose \u2014 an AsDetail\n row's `__sparkBreadcrumbs` is keyed by column, not by id, so there are\n no labels for the members. Text beats a row of raw ids.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(row | asDetailCellValue:attr:col:asDetailReferenceOptions())\"\n [rendererValue]=\"row[col.name]\"\n [item]=\"row\"\n [link]=\"col.dataType === 'Reference' && col.referenceType\n ? (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes())\n : null\" />\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\"\n class=\"opacity-100\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'image') {\n @let imageSrc = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (imageSrc) {\n <img [src]=\"imageSrc\" [alt]=\"(attr.label | resolveTranslation) || attr.name\" loading=\"lazy\"\n class=\"img-fluid rounded\" style=\"max-height: 12rem;\">\n } @else {\n -\n }\n } @else if (attr.dataType === 'url') {\n @let hrefVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (hrefVal) {\n <a [href]=\"hrefVal\" target=\"_blank\" rel=\"noopener noreferrer\">{{ hrefVal }}</a>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n @let chips = (attr.name | referenceChips:item());\n @if (chips.length) {\n <div class=\"d-flex flex-wrap gap-1\">\n @for (chip of chips; track chip.id) {\n @let chipRoute = (attr.referenceType ? (attr.referenceType | referenceLinkRoute:chip.id:allEntityTypes()) : null);\n @if (chipRoute) {\n <a class=\"spark-chip\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n }\n </div>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.referenceType) {\n @let refRoute = (attr.referenceType | referenceLinkRoute:(attr.name | rawAttributeValue:item()):allEntityTypes());\n @if (refRoute && (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes())) {\n <a [routerLink]=\"refRoute\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) }}</a>\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else if (attr.dataType === 'MultiLineString') {\n <div class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else if (attr.dataType === 'date' || attr.dataType === 'datetime') {\n <!--\n Formatted the same way, and in the same timezone, as the grid renders it.\n\n Before this, a detail page printed the raw wire value \u2014 `2026-12-31T23:59:00-08:00`\n in a definition list \u2014 while the grid showed `01/01/2027, 08:59` for the same\n document. Not a rounding difference: a DateTimeOffset whose offset differs from the\n viewer's can disagree on the DATE, so the two pages named different days for one car.\n\n The grid's meaning wins because it is the one a reader can act on: a timestamp is an\n instant, shown in the viewer's own zone. The originating offset is still on the wire\n for code that needs it \u2014 server-side logic, exports, \"which country's morning was\n this?\" \u2014 it simply is not what a viewer is shown by default.\n -->\n @let when = (attr.name | rawAttributeValue:item()) | parsedDate;\n @if (when) {\n {{ when | date:(attr.dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @if (et.queries?.length) {\n @for (queryAlias of et.queries; track queryAlias) {\n <spark-query-card [queryId]=\"queryAlias\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\"\n [iconTemplate]=\"queryIconTemplate()\" [captionTemplate]=\"queryCaptionTemplate()\"\n [actionsTemplate]=\"queryActionsTemplate()\" />\n }\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n", styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}.spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.container{--spark-content-inset-inline: 0px}.spark-multiline-value{white-space:pre-wrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["color", "outline"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["color", "navStyle"] }, { kind: "component", type: BsContainerComponent, selector: "bs-container" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "component", type: BsPriorityNavComponent, selector: "bs-priority-nav", inputs: ["moreLabel", "moreLabelTemplate", "collapseAt", "overflowFrom", "hideEmptyMore", "ariaLabel"], outputs: ["overflowChange"] }, { kind: "directive", type: BsPriorityNavItemDirective, selector: "[bsPriorityNavItem]", inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"] }, { kind: "component", type: BsTableComponent, selector: "bs-table", inputs: ["isResponsive", "striped", "hover", "border", "ariaRowCount"] }, { kind: "component", type: BsTabControlComponent, selector: "bs-tab-control", inputs: ["border", "selectFirstTab", "tabsPosition"] }, { kind: "component", type: BsTabPageComponent, selector: "bs-tab-page", inputs: ["disabled"] }, { kind: "directive", type: BsTabPageHeaderDirective, selector: "[bsTabPageHeader]" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "component", type: SparkQueryCardComponent, selector: "spark-query-card", inputs: ["queryId", "parentId", "parentType", "reloadToken", "data", "search", "iconTemplate", "captionTemplate", "actionsTemplate", "rowRoute"], outputs: ["error", "rowClicked", "customActionExecuted"] }, { kind: "component", type: SparkGridCellComponent, selector: "spark-grid-cell", inputs: ["column", "display", "rendererValue", "item", "chips", "link"] }, { kind: "component", type: SparkAttributeDescriptionComponent, selector: "spark-attribute-description", inputs: ["description", "position"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }, { kind: "pipe", type: RawAttributeValuePipe, name: "rawAttributeValue" }, { kind: "pipe", type: AsDetailColumnsPipe, name: "asDetailColumns" }, { kind: "pipe", type: AsDetailCellValuePipe, name: "asDetailCellValue" }, { kind: "pipe", type: ArrayValuePipe, name: "arrayValue" }, { kind: "pipe", type: ReferenceLinkRoutePipe, name: "referenceLinkRoute" }, { kind: "pipe", type: ReferenceChipsPipe, name: "referenceChips" }, { kind: "pipe", type: ParsedDatePipe, name: "parsedDate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
400
- }
401
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkPoDetailComponent, decorators: [{
402
- type: Component,
403
- args: [{ selector: 'spark-po-detail', imports: [CommonModule, NgTemplateOutlet, NgComponentOutlet, RouterModule, BsAlertComponent, BsCardComponent, BsCardHeaderComponent, BsContainerComponent, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsPriorityNavComponent, BsPriorityNavItemDirective, BsTableComponent, BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective, BsSpinnerComponent, SparkIconComponent, SparkQueryCardComponent, SparkGridCellComponent, ResolveTranslationPipe, TranslateKeyPipe, AttributeValuePipe, RawAttributeValuePipe, AsDetailColumnsPipe, AsDetailCellValuePipe, ArrayValuePipe, ReferenceLinkRoutePipe, ReferenceChipsPipe, ParsedDatePipe, SparkAttributeDescriptionComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<bs-container>\n<div class=\"container\">\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n } @else if (item(); as currentItem) {\n @if (entityType(); as et) {\n <div class=\"spark-actionbar py-2\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <button *bsPriorityNavItem=\"1\" class=\"btn btn-outline-secondary\" (click)=\"onBack()\">\n <spark-icon name=\"arrow-left\" /> {{ 'common.back' | t }}\n </button>\n @if (canEdit()) {\n <button *bsPriorityNavItem=\"2\" class=\"btn btn-primary\" (click)=\"onEdit()\">\n <spark-icon name=\"pencil\" /> {{ 'common.edit' | t }}\n </button>\n }\n @if (canDelete()) {\n <button *bsPriorityNavItem=\"3\" class=\"btn btn-danger\" (click)=\"onDelete()\">\n <spark-icon name=\"trash\" /> {{ 'common.delete' | t }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of visibleCustomActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [disabled]=\"runningAction() !== null\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n </bs-priority-nav>\n </div>\n <h2>{{ currentItem.breadcrumb || currentItem.name }}</h2>\n\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"detailTabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #detailTabContent let-tab>\n @if (tab.id === '__default__') {\n @let ungroupedAttrs = ungroupedAttributes();\n @if (ungroupedAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of ungroupedAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #detailAttrField let-attr let-currentItem=\"item\">\n <dt [sm]=\"3\">{{ (attr.label | resolveTranslation) || attr.name }}<spark-attribute-description [description]=\"attr.description\" /></dt>\n <dd [sm]=\"9\">\n @if (getDetailRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getDetailRendererInputs(rendererType, attr, currentItem)\"></ng-container>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n <!--\n Shared with the query grid. This cell used to hand-roll the renderer\n dispatch and print everything else as text, so a `boolean` column read\n \"true\" here and rendered a checkbox in a grid, and a `color` showed its\n hex. Only the VALUE is resolved locally: these rows are plain\n dictionaries, not PersistentObjects.\n\n `chips` is left unset for a reference array on purpose \u2014 an AsDetail\n row's `__sparkBreadcrumbs` is keyed by column, not by id, so there are\n no labels for the members. Text beats a row of raw ids.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(row | asDetailCellValue:attr:col:asDetailReferenceOptions())\"\n [rendererValue]=\"row[col.name]\"\n [item]=\"row\"\n [link]=\"col.dataType === 'Reference' && col.referenceType\n ? (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes())\n : null\" />\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\"\n class=\"opacity-100\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'image') {\n @let imageSrc = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (imageSrc) {\n <img [src]=\"imageSrc\" [alt]=\"(attr.label | resolveTranslation) || attr.name\" loading=\"lazy\"\n class=\"img-fluid rounded\" style=\"max-height: 12rem;\">\n } @else {\n -\n }\n } @else if (attr.dataType === 'url') {\n @let hrefVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (hrefVal) {\n <a [href]=\"hrefVal\" target=\"_blank\" rel=\"noopener noreferrer\">{{ hrefVal }}</a>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n @let chips = (attr.name | referenceChips:item());\n @if (chips.length) {\n <div class=\"d-flex flex-wrap gap-1\">\n @for (chip of chips; track chip.id) {\n @let chipRoute = (attr.referenceType ? (attr.referenceType | referenceLinkRoute:chip.id:allEntityTypes()) : null);\n @if (chipRoute) {\n <a class=\"spark-chip\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n }\n </div>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.referenceType) {\n @let refRoute = (attr.referenceType | referenceLinkRoute:(attr.name | rawAttributeValue:item()):allEntityTypes());\n @if (refRoute && (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes())) {\n <a [routerLink]=\"refRoute\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) }}</a>\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else if (attr.dataType === 'MultiLineString') {\n <div class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else if (attr.dataType === 'date' || attr.dataType === 'datetime') {\n <!--\n Formatted the same way, and in the same timezone, as the grid renders it.\n\n Before this, a detail page printed the raw wire value \u2014 `2026-12-31T23:59:00-08:00`\n in a definition list \u2014 while the grid showed `01/01/2027, 08:59` for the same\n document. Not a rounding difference: a DateTimeOffset whose offset differs from the\n viewer's can disagree on the DATE, so the two pages named different days for one car.\n\n The grid's meaning wins because it is the one a reader can act on: a timestamp is an\n instant, shown in the viewer's own zone. The originating offset is still on the wire\n for code that needs it \u2014 server-side logic, exports, \"which country's morning was\n this?\" \u2014 it simply is not what a viewer is shown by default.\n -->\n @let when = (attr.name | rawAttributeValue:item()) | parsedDate;\n @if (when) {\n {{ when | date:(attr.dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @if (et.queries?.length) {\n @for (queryAlias of et.queries; track queryAlias) {\n <spark-query-card [queryId]=\"queryAlias\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\"\n [iconTemplate]=\"queryIconTemplate()\" [captionTemplate]=\"queryCaptionTemplate()\"\n [actionsTemplate]=\"queryActionsTemplate()\" />\n }\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n", styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}.spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.container{--spark-content-inset-inline: 0px}.spark-multiline-value{white-space:pre-wrap}\n"] }]
404
- }], ctorParameters: () => [], propDecorators: { showCustomActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCustomActions", required: false }] }], extraActionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraActionsTemplate", required: false }] }], extraContentTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraContentTemplate", required: false }] }], queryIconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryIconTemplate", required: false }] }], queryCaptionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryCaptionTemplate", required: false }] }], queryActionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryActionsTemplate", required: false }] }], edited: [{ type: i0.Output, args: ["edited"] }], deleted: [{ type: i0.Output, args: ["deleted"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }] } });
405
-
406
- /**
407
- * Generated bundle index. Do not edit.
408
- */
409
-
1
+ import * as i0 from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output, signal, untracked } from "@angular/core";
3
+ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
4
+ import * as i1 from "@angular/common";
5
+ import { CommonModule, NgComponentOutlet, NgTemplateOutlet } from "@angular/common";
6
+ import * as i2 from "@angular/router";
7
+ import { ActivatedRoute, Router, RouterModule } from "@angular/router";
8
+ import { Color } from "@mintplayer/ng-bootstrap";
9
+ import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
10
+ import { BsBadgeComponent } from "@mintplayer/ng-bootstrap/badge";
11
+ import { BsCardComponent, BsCardHeaderComponent } from "@mintplayer/ng-bootstrap/card";
12
+ import { BsContainerComponent } from "@mintplayer/ng-bootstrap/container";
13
+ import { BsGridColumnDirective, BsGridComponent, BsGridRowDirective } from "@mintplayer/ng-bootstrap/grid";
14
+ import { BsPriorityNavComponent, BsPriorityNavItemDirective } from "@mintplayer/ng-bootstrap/priority-nav";
15
+ import { BsTableComponent } from "@mintplayer/ng-bootstrap/table";
16
+ import { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } from "@mintplayer/ng-bootstrap/tab-control";
17
+ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
18
+ import { SparkLanguageService, SparkReturnNavigationService, SparkService } from "@mintplayer/ng-spark/services";
19
+ import { SparkAttributeRefreshService, SparkQueryRefreshService } from "@mintplayer/ng-spark/client-operations";
20
+ import { ArrayValuePipe, AsDetailCellValuePipe, AsDetailColumnsPipe, AttributeValuePipe, ParsedDatePipe, RawAttributeValuePipe, ReferenceChipsPipe, ReferenceLinkRoutePipe, ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
21
+ import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
22
+ import { SparkAttributeDescriptionComponent } from "@mintplayer/ng-spark/attribute-description";
23
+ import { SparkGridCellComponent, SparkQueryCardComponent } from "@mintplayer/ng-spark/grid";
24
+ import { SPARK_ATTRIBUTE_RENDERERS, rendererValue, withDeclaredInputs } from "@mintplayer/ng-spark/renderers";
25
+ import { ShowedOn, filterDetailActions, hasShowedOnFlag, subQueriesOf } from "@mintplayer/ng-spark/models";
26
+ import { SPARK_DETAIL_ACTIONS, SPARK_DETAIL_PANELS, orderSparkExtensions, parseSparkDeletedParam } from "@mintplayer/ng-spark/panels";
27
+ import { combineLatest } from "rxjs";
28
+ var SparkPoDetailComponent = class SparkPoDetailComponent {
29
+ route = inject(ActivatedRoute);
30
+ router = inject(Router);
31
+ sparkService = inject(SparkService);
32
+ returnNavigation = inject(SparkReturnNavigationService);
33
+ queryRefresh = inject(SparkQueryRefreshService);
34
+ attributeRefresh = inject(SparkAttributeRefreshService);
35
+ lang = inject(SparkLanguageService);
36
+ rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);
37
+ detailPanels = orderSparkExtensions(inject(SPARK_DETAIL_PANELS, { optional: true }), (p) => p.order);
38
+ detailActions = orderSparkExtensions(inject(SPARK_DETAIL_ACTIONS, { optional: true }), (a) => a.priority ?? 60);
39
+ showCustomActions = input(true, ...ngDevMode ? [{ debugName: "showCustomActions" }] : /* istanbul ignore next */ []);
40
+ extraActionsTemplate = input(null, ...ngDevMode ? [{ debugName: "extraActionsTemplate" }] : /* istanbul ignore next */ []);
41
+ extraContentTemplate = input(null, ...ngDevMode ? [{ debugName: "extraContentTemplate" }] : /* istanbul ignore next */ []);
42
+ queryIconTemplate = input(null, ...ngDevMode ? [{ debugName: "queryIconTemplate" }] : /* istanbul ignore next */ []);
43
+ queryCaptionTemplate = input(null, ...ngDevMode ? [{ debugName: "queryCaptionTemplate" }] : /* istanbul ignore next */ []);
44
+ queryActionsTemplate = input(null, ...ngDevMode ? [{ debugName: "queryActionsTemplate" }] : /* istanbul ignore next */ []);
45
+ edited = output();
46
+ deleted = output();
47
+ customActionExecuted = output();
48
+ colors = Color;
49
+ errorMessage = signal(null, ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
50
+ entityType = signal(null, ...ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []);
51
+ subQueries = computed(() => subQueriesOf(this.entityType()), ...ngDevMode ? [{ debugName: "subQueries" }] : /* istanbul ignore next */ []);
52
+ allEntityTypes = signal([], ...ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []);
53
+ item = signal(null, ...ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []);
54
+ lookupReferenceOptions = signal({}, ...ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []);
55
+ asDetailTypes = signal({}, ...ngDevMode ? [{ debugName: "asDetailTypes" }] : /* istanbul ignore next */ []);
56
+ asDetailReferenceOptions = signal({}, ...ngDevMode ? [{ debugName: "asDetailReferenceOptions" }] : /* istanbul ignore next */ []);
57
+ type = "";
58
+ id = "";
59
+ canEdit = signal(false, ...ngDevMode ? [{ debugName: "canEdit" }] : /* istanbul ignore next */ []);
60
+ canDelete = signal(false, ...ngDevMode ? [{ debugName: "canDelete" }] : /* istanbul ignore next */ []);
61
+ customActions = signal([], ...ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []);
62
+ permissions = signal(null, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
63
+ deletedMode = signal(null, ...ngDevMode ? [{ debugName: "deletedMode" }] : /* istanbul ignore next */ []);
64
+ isDeletedView = computed(() => this.deletedMode() === "only", ...ngDevMode ? [{ debugName: "isDeletedView" }] : /* istanbul ignore next */ []);
65
+ detailContext = computed(() => {
66
+ const item = this.item();
67
+ const entityType = this.entityType();
68
+ if (!item || !entityType) return null;
69
+ return {
70
+ type: this.type,
71
+ id: this.id,
72
+ item,
73
+ entityType,
74
+ permissions: this.permissions(),
75
+ deleted: this.deletedMode(),
76
+ reload: () => this.load()
77
+ };
78
+ }, ...ngDevMode ? [{ debugName: "detailContext" }] : /* istanbul ignore next */ []);
79
+ visibleCustomActions = computed(() => {
80
+ const withheld = this.item()?.disabledActions;
81
+ if (!withheld?.length) return this.customActions();
82
+ const lowered = new Set(withheld.map((name) => name.toLowerCase()));
83
+ return this.customActions().filter((action) => !lowered.has(action.name.toLowerCase()));
84
+ }, ...ngDevMode ? [{ debugName: "visibleCustomActions" }] : /* istanbul ignore next */ []);
85
+ constructor() {
86
+ combineLatest([this.route.paramMap, this.route.queryParamMap]).pipe(takeUntilDestroyed()).subscribe(([params, query]) => this.onParamsChange(params, query.get("deleted")));
87
+ effect(() => {
88
+ const objectTypeId = this.entityType()?.id;
89
+ if (!this.attributeRefresh.tokenFor(objectTypeId, this.id)) return;
90
+ const patches = this.attributeRefresh.patchesFor(objectTypeId, this.id);
91
+ untracked(() => this.applyAttributePatches(patches));
92
+ });
93
+ }
94
+ applyAttributePatches(patches) {
95
+ const current = this.item();
96
+ if (!current) return;
97
+ let changed = false;
98
+ const attributes = current.attributes.map((attribute) => {
99
+ if (!Object.prototype.hasOwnProperty.call(patches, attribute.name)) return attribute;
100
+ const patch = patches[attribute.name];
101
+ const next = { ...attribute };
102
+ let patched = false;
103
+ if (patch.value !== void 0 && next.value !== patch.value) {
104
+ next.value = patch.value;
105
+ patched = true;
106
+ }
107
+ if (attribute.dataType === "AsDetail") {
108
+ if (patch.object !== void 0 && next.object !== patch.object) {
109
+ next.object = patch.object;
110
+ patched = true;
111
+ }
112
+ if (patch.objects !== void 0 && next.objects !== patch.objects) {
113
+ next.objects = patch.objects;
114
+ patched = true;
115
+ }
116
+ }
117
+ if (!patched) return attribute;
118
+ changed = true;
119
+ return next;
120
+ });
121
+ if (changed) this.item.set({
122
+ ...current,
123
+ attributes
124
+ });
125
+ }
126
+ async onParamsChange(params, deleted = null) {
127
+ this.type = params.get("type") || "";
128
+ this.id = params.get("id") || "";
129
+ const mode = parseSparkDeletedParam(deleted);
130
+ this.deletedMode.set(mode === "exclude" ? null : mode);
131
+ await this.load();
132
+ }
133
+ loadItem() {
134
+ const deleted = this.deletedMode();
135
+ return deleted ? this.sparkService.get(this.type, this.id, { deleted }) : this.sparkService.get(this.type, this.id);
136
+ }
137
+ async load() {
138
+ this.errorMessage.set(null);
139
+ try {
140
+ const [entityTypes, item] = await Promise.all([this.sparkService.getEntityTypes(), this.loadItem()]);
141
+ this.allEntityTypes.set(entityTypes);
142
+ this.entityType.set(entityTypes.find((t) => t.id === this.type || t.alias === this.type) || null);
143
+ this.item.set(item);
144
+ this.loadLookupReferenceOptions();
145
+ this.loadAsDetailTypes();
146
+ const et = this.entityType();
147
+ if (et) {
148
+ const [permissions, actions] = await Promise.all([this.sparkService.getPermissions(et.id), this.sparkService.getCustomActions(et.id)]);
149
+ const can = item.can;
150
+ const withheld = new Set((item.disabledActions ?? []).map((name) => name.toLowerCase()));
151
+ const deletedView = this.isDeletedView();
152
+ this.permissions.set(permissions);
153
+ this.canEdit.set(!deletedView && (can ? can.edit : permissions.canEdit) && !withheld.has("edit") && !withheld.has("save"));
154
+ this.canDelete.set(!deletedView && (can ? can.delete : permissions.canDelete) && !withheld.has("delete"));
155
+ this.customActions.set(deletedView ? [] : filterDetailActions(actions));
156
+ }
157
+ } catch (e) {
158
+ const error = e;
159
+ this.errorMessage.set(error.error?.error || error.message || "An unexpected error occurred");
160
+ }
161
+ }
162
+ visibleAttributes = computed(() => {
163
+ return this.entityType()?.attributes.filter((a) => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject)).sort((a, b) => a.order - b.order) || [];
164
+ }, ...ngDevMode ? [{ debugName: "visibleAttributes" }] : /* istanbul ignore next */ []);
165
+ static DEFAULT_TAB = {
166
+ id: "__default__",
167
+ name: "Algemeen",
168
+ label: {
169
+ nl: "Algemeen",
170
+ en: "General"
171
+ },
172
+ order: 0
173
+ };
174
+ ungroupedAttributes = computed(() => {
175
+ const attrs = this.visibleAttributes();
176
+ const groupIds = new Set((this.entityType()?.groups || []).map((g) => g.id));
177
+ return attrs.filter((a) => !a.group || !groupIds.has(a.group));
178
+ }, ...ngDevMode ? [{ debugName: "ungroupedAttributes" }] : /* istanbul ignore next */ []);
179
+ resolvedTabs = computed(() => {
180
+ const et = this.entityType();
181
+ const definedTabs = et?.tabs?.length ? [...et.tabs].sort((a, b) => a.order - b.order) : [];
182
+ const hasUngroupedAttrs = this.ungroupedAttributes().length > 0;
183
+ const hasUntabbedGroups = (et?.groups || []).some((g) => !g.tab);
184
+ if (hasUngroupedAttrs || hasUntabbedGroups || definedTabs.length === 0) return [SparkPoDetailComponent.DEFAULT_TAB, ...definedTabs];
185
+ return definedTabs;
186
+ }, ...ngDevMode ? [{ debugName: "resolvedTabs" }] : /* istanbul ignore next */ []);
187
+ groupsForTab(tab) {
188
+ const groups = this.entityType()?.groups || [];
189
+ if (tab.id === "__default__") return groups.filter((g) => !g.tab).sort((a, b) => a.order - b.order);
190
+ return groups.filter((g) => g.tab === tab.id).sort((a, b) => a.order - b.order);
191
+ }
192
+ attrsForGroup(group) {
193
+ return this.visibleAttributes().filter((a) => a.group === group.id);
194
+ }
195
+ getDetailRendererComponent(attr) {
196
+ if (!attr.renderer) return null;
197
+ return this.rendererRegistry.find((r) => r.name === attr.renderer)?.detailComponent ?? null;
198
+ }
199
+ getDetailRendererInputs(component, attr, item) {
200
+ const itemAttr = item.attributes.find((a) => a.name === attr.name);
201
+ const formData = {};
202
+ for (const a of item.attributes) formData[a.name] = rendererValue(a);
203
+ return withDeclaredInputs(component, {
204
+ value: rendererValue(itemAttr),
205
+ attribute: attr,
206
+ options: attr.rendererOptions,
207
+ formData,
208
+ item
209
+ });
210
+ }
211
+ getAsDetailCellRendererComponent(col) {
212
+ if (!col.renderer) return null;
213
+ return this.rendererRegistry.find((r) => r.name === col.renderer)?.columnComponent ?? null;
214
+ }
215
+ getAsDetailCellRendererInputs(component, row, col) {
216
+ return withDeclaredInputs(component, {
217
+ value: row[col.name],
218
+ attribute: col,
219
+ options: col.rendererOptions,
220
+ item: row
221
+ });
222
+ }
223
+ async loadLookupReferenceOptions() {
224
+ const lookupAttrs = this.visibleAttributes().filter((a) => a.lookupReferenceType);
225
+ if (lookupAttrs.length === 0) return;
226
+ const lookupNames = [...new Set(lookupAttrs.map((a) => a.lookupReferenceType))];
227
+ const entries = await Promise.all(lookupNames.map(async (name) => {
228
+ return [name, await this.sparkService.getLookupReference(name).catch(() => null)];
229
+ }));
230
+ this.lookupReferenceOptions.set(entries.reduce((acc, [k, v]) => v ? {
231
+ ...acc,
232
+ [k]: v
233
+ } : acc, {}));
234
+ }
235
+ async loadAsDetailTypes() {
236
+ const asDetailAttrs = this.visibleAttributes().filter((a) => a.dataType === "AsDetail" && a.isArray && a.asDetailType);
237
+ if (asDetailAttrs.length === 0) return;
238
+ const types = this.allEntityTypes();
239
+ const newAsDetailTypes = {};
240
+ const embedded = this.entityType()?.detailTypes ?? [];
241
+ for (const attr of asDetailAttrs) {
242
+ const asDetailType = types.find((t) => t.clrType === attr.asDetailType) ?? embedded.find((t) => t.clrType === attr.asDetailType);
243
+ if (asDetailType) {
244
+ newAsDetailTypes[attr.name] = asDetailType;
245
+ const refCols = asDetailType.attributes.filter((a) => a.dataType === "Reference" && a.query);
246
+ if (refCols.length > 0) {
247
+ const refEntries = await Promise.all(refCols.map(async (col) => {
248
+ const result = await this.sparkService.executeQueryByName(col.query, {
249
+ parentId: this.id,
250
+ parentType: this.type
251
+ });
252
+ return [col.name, result.items];
253
+ }));
254
+ this.asDetailReferenceOptions.update((prev) => ({
255
+ ...prev,
256
+ [attr.name]: refEntries.reduce((acc, [k, v]) => ({
257
+ ...acc,
258
+ [k]: v
259
+ }), {})
260
+ }));
261
+ }
262
+ }
263
+ }
264
+ this.asDetailTypes.set(newAsDetailTypes);
265
+ }
266
+ customActionClass(action) {
267
+ const variant = action.variant?.toLowerCase();
268
+ switch (variant) {
269
+ case "danger":
270
+ case "warning":
271
+ case "primary":
272
+ case "secondary":
273
+ case "success": return `btn btn-${variant}`;
274
+ default: return "btn btn-outline-primary";
275
+ }
276
+ }
277
+ runningAction = signal(null, ...ngDevMode ? [{ debugName: "runningAction" }] : /* istanbul ignore next */ []);
278
+ async onCustomAction(action) {
279
+ if (this.runningAction()) return;
280
+ if (action.confirmationMessageKey) {
281
+ const message = this.lang.t(action.confirmationMessageKey) || "Are you sure?";
282
+ if (!confirm(message)) return;
283
+ }
284
+ this.runningAction.set(action.name);
285
+ try {
286
+ await this.sparkService.executeCustomAction(this.type, action.name, this.item() || void 0);
287
+ this.customActionExecuted.emit({
288
+ action,
289
+ item: this.item()
290
+ });
291
+ if (action.refreshOnCompleted) {
292
+ const item = await this.loadItem();
293
+ this.item.set(item);
294
+ for (const subQuery of this.subQueries()) this.queryRefresh.request(subQuery.query);
295
+ }
296
+ } catch (e) {
297
+ const err = e;
298
+ this.errorMessage.set(err.error?.error || err.message || "Action failed");
299
+ } finally {
300
+ this.runningAction.set(null);
301
+ }
302
+ }
303
+ onEdit() {
304
+ this.edited.emit();
305
+ this.router.navigate([
306
+ "/po",
307
+ this.type,
308
+ this.id,
309
+ "edit"
310
+ ]);
311
+ }
312
+ async onDelete() {
313
+ if (confirm(this.lang.t("common.confirmDelete"))) {
314
+ await this.sparkService.delete(this.type, this.id);
315
+ this.deleted.emit();
316
+ await this.returnNavigation.returnToList(this.entityType()?.name);
317
+ }
318
+ }
319
+ onBack() {
320
+ window.history.back();
321
+ }
322
+ static ɵfac = i0.ɵɵngDeclareFactory({
323
+ minVersion: "12.0.0",
324
+ version: "22.2.0",
325
+ ngImport: i0,
326
+ type: SparkPoDetailComponent,
327
+ deps: [],
328
+ target: i0.ɵɵFactoryTarget.Component
329
+ });
330
+ static ɵcmp = i0.ɵɵngDeclareComponent({
331
+ minVersion: "17.0.0",
332
+ version: "22.2.0",
333
+ type: SparkPoDetailComponent,
334
+ isStandalone: true,
335
+ selector: "spark-po-detail",
336
+ inputs: {
337
+ showCustomActions: {
338
+ classPropertyName: "showCustomActions",
339
+ publicName: "showCustomActions",
340
+ isSignal: true,
341
+ isRequired: false,
342
+ transformFunction: null
343
+ },
344
+ extraActionsTemplate: {
345
+ classPropertyName: "extraActionsTemplate",
346
+ publicName: "extraActionsTemplate",
347
+ isSignal: true,
348
+ isRequired: false,
349
+ transformFunction: null
350
+ },
351
+ extraContentTemplate: {
352
+ classPropertyName: "extraContentTemplate",
353
+ publicName: "extraContentTemplate",
354
+ isSignal: true,
355
+ isRequired: false,
356
+ transformFunction: null
357
+ },
358
+ queryIconTemplate: {
359
+ classPropertyName: "queryIconTemplate",
360
+ publicName: "queryIconTemplate",
361
+ isSignal: true,
362
+ isRequired: false,
363
+ transformFunction: null
364
+ },
365
+ queryCaptionTemplate: {
366
+ classPropertyName: "queryCaptionTemplate",
367
+ publicName: "queryCaptionTemplate",
368
+ isSignal: true,
369
+ isRequired: false,
370
+ transformFunction: null
371
+ },
372
+ queryActionsTemplate: {
373
+ classPropertyName: "queryActionsTemplate",
374
+ publicName: "queryActionsTemplate",
375
+ isSignal: true,
376
+ isRequired: false,
377
+ transformFunction: null
378
+ }
379
+ },
380
+ outputs: {
381
+ edited: "edited",
382
+ deleted: "deleted",
383
+ customActionExecuted: "customActionExecuted"
384
+ },
385
+ ngImport: i0,
386
+ template: "<bs-container>\n<div class=\"container\">\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n } @else if (item(); as currentItem) {\n @if (entityType(); as et) {\n <div class=\"spark-actionbar py-2\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <button *bsPriorityNavItem=\"1\" class=\"btn btn-outline-secondary\" (click)=\"onBack()\">\n <spark-icon name=\"arrow-left\" /> {{ 'common.back' | t }}\n </button>\n @if (canEdit()) {\n <button *bsPriorityNavItem=\"2\" class=\"btn btn-primary\" (click)=\"onEdit()\">\n <spark-icon name=\"pencil\" /> {{ 'common.edit' | t }}\n </button>\n }\n @if (canDelete()) {\n <button *bsPriorityNavItem=\"3\" class=\"btn btn-danger\" (click)=\"onDelete()\">\n <spark-icon name=\"trash\" /> {{ 'common.delete' | t }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of visibleCustomActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [disabled]=\"runningAction() !== null\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_DETAIL_ACTIONS): each decides for itself whether it applies. -->\n @if (detailContext(); as ctx) {\n @for (action of detailActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n <h2>\n {{ currentItem.breadcrumb || currentItem.name }}\n @if (isDeletedView()) {\n <bs-badge [type]=\"colors.secondary\" class=\"align-middle ms-2 spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"detailTabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #detailTabContent let-tab>\n @if (tab.id === '__default__') {\n @let ungroupedAttrs = ungroupedAttributes();\n @if (ungroupedAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of ungroupedAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #detailAttrField let-attr let-currentItem=\"item\">\n <dt [sm]=\"3\">{{ (attr.label | resolveTranslation) || attr.name }}<spark-attribute-description [description]=\"attr.description\" /></dt>\n <dd [sm]=\"9\">\n @if (getDetailRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getDetailRendererInputs(rendererType, attr, currentItem)\"></ng-container>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n <!--\n Shared with the query grid. This cell used to hand-roll the renderer\n dispatch and print everything else as text, so a `boolean` column read\n \"true\" here and rendered a checkbox in a grid, and a `color` showed its\n hex. Only the VALUE is resolved locally: these rows are plain\n dictionaries, not PersistentObjects.\n\n `chips` is left unset for a reference array on purpose — an AsDetail\n row's `__sparkBreadcrumbs` is keyed by column, not by id, so there are\n no labels for the members. Text beats a row of raw ids.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(row | asDetailCellValue:attr:col:asDetailReferenceOptions())\"\n [rendererValue]=\"row[col.name]\"\n [item]=\"row\"\n [link]=\"col.dataType === 'Reference' && col.referenceType\n ? (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes())\n : null\" />\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\"\n class=\"opacity-100\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'image') {\n @let imageSrc = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (imageSrc) {\n <img [src]=\"imageSrc\" [alt]=\"(attr.label | resolveTranslation) || attr.name\" loading=\"lazy\"\n class=\"img-fluid rounded\" style=\"max-height: 12rem;\">\n } @else {\n -\n }\n } @else if (attr.dataType === 'url') {\n @let hrefVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (hrefVal) {\n <a [href]=\"hrefVal\" target=\"_blank\" rel=\"noopener noreferrer\">{{ hrefVal }}</a>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n @let chips = (attr.name | referenceChips:item());\n @if (chips.length) {\n <div class=\"d-flex flex-wrap gap-1\">\n @for (chip of chips; track chip.id) {\n @let chipRoute = (attr.referenceType ? (attr.referenceType | referenceLinkRoute:chip.id:allEntityTypes()) : null);\n @if (chipRoute) {\n <a class=\"spark-chip\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n }\n </div>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.referenceType) {\n @let refRoute = (attr.referenceType | referenceLinkRoute:(attr.name | rawAttributeValue:item()):allEntityTypes());\n @if (refRoute && (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes())) {\n <a [routerLink]=\"refRoute\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) }}</a>\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else if (attr.dataType === 'MultiLineString') {\n <div class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else if (attr.dataType === 'date' || attr.dataType === 'datetime') {\n <!--\n Formatted the same way, and in the same timezone, as the grid renders it.\n\n Before this, a detail page printed the raw wire value — `2026-12-31T23:59:00-08:00`\n in a definition list — while the grid showed `01/01/2027, 08:59` for the same\n document. Not a rounding difference: a DateTimeOffset whose offset differs from the\n viewer's can disagree on the DATE, so the two pages named different days for one car.\n\n The grid's meaning wins because it is the one a reader can act on: a timestamp is an\n instant, shown in the viewer's own zone. The originating offset is still on the wire\n for code that needs it — server-side logic, exports, \"which country's morning was\n this?\" — it simply is not what a viewer is shown by default.\n -->\n @let when = (attr.name | rawAttributeValue:item()) | parsedDate;\n @if (when) {\n {{ when | date:(attr.dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @for (subQuery of subQueries(); track subQuery.query) {\n <!-- The entry's selectionMode overrides the query's own for this parent (#460, D17). -->\n <spark-query-card [queryId]=\"subQuery.query\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\"\n [selectionMode]=\"subQuery.selectionMode ?? null\"\n [parentDeleted]=\"isDeletedView()\"\n [iconTemplate]=\"queryIconTemplate()\" [captionTemplate]=\"queryCaptionTemplate()\"\n [actionsTemplate]=\"queryActionsTemplate()\" />\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n <!-- Add-on panels (SPARK_DETAIL_PANELS), e.g. <spark-po-history>. -->\n @if (detailContext(); as ctx) {\n @for (panel of detailPanels; track panel.id) {\n <div class=\"spark-detail-panel m-3\">\n <ng-container *ngComponentOutlet=\"panel.component; inputs: { context: ctx }\" />\n </div>\n }\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
387
+ styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}.spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.container{--spark-content-inset-inline: 0px}.spark-multiline-value{white-space:pre-wrap}\n"],
388
+ dependencies: [
389
+ {
390
+ kind: "ngmodule",
391
+ type: CommonModule
392
+ },
393
+ {
394
+ kind: "directive",
395
+ type: i1.NgComponentOutlet,
396
+ selector: "[ngComponentOutlet]",
397
+ inputs: [
398
+ "ngComponentOutlet",
399
+ "ngComponentOutletInputs",
400
+ "ngComponentOutletInjector",
401
+ "ngComponentOutletEnvironmentInjector",
402
+ "ngComponentOutletContent",
403
+ "ngComponentOutletNgModule"
404
+ ],
405
+ exportAs: ["ngComponentOutlet"]
406
+ },
407
+ {
408
+ kind: "directive",
409
+ type: i1.NgTemplateOutlet,
410
+ selector: "[ngTemplateOutlet]",
411
+ inputs: [
412
+ "ngTemplateOutletContext",
413
+ "ngTemplateOutlet",
414
+ "ngTemplateOutletInjector"
415
+ ]
416
+ },
417
+ {
418
+ kind: "ngmodule",
419
+ type: RouterModule
420
+ },
421
+ {
422
+ kind: "directive",
423
+ type: i2.RouterLink,
424
+ selector: "[routerLink]",
425
+ inputs: [
426
+ "target",
427
+ "queryParams",
428
+ "fragment",
429
+ "queryParamsHandling",
430
+ "state",
431
+ "info",
432
+ "relativeTo",
433
+ "preserveFragment",
434
+ "skipLocationChange",
435
+ "replaceUrl",
436
+ "browserUrl",
437
+ "routerLink"
438
+ ]
439
+ },
440
+ {
441
+ kind: "component",
442
+ type: BsAlertComponent,
443
+ selector: "bs-alert",
444
+ inputs: [
445
+ "type",
446
+ "announce",
447
+ "isVisible"
448
+ ],
449
+ outputs: ["isVisibleChange", "afterOpenedOrClosed"]
450
+ },
451
+ {
452
+ kind: "component",
453
+ type: BsBadgeComponent,
454
+ selector: "bs-badge",
455
+ inputs: [
456
+ "type",
457
+ "unit",
458
+ "decorative"
459
+ ]
460
+ },
461
+ {
462
+ kind: "component",
463
+ type: BsCardComponent,
464
+ selector: "bs-card",
465
+ inputs: ["color", "outline"]
466
+ },
467
+ {
468
+ kind: "component",
469
+ type: BsCardHeaderComponent,
470
+ selector: "bs-card-header",
471
+ inputs: ["color", "navStyle"]
472
+ },
473
+ {
474
+ kind: "component",
475
+ type: BsContainerComponent,
476
+ selector: "bs-container"
477
+ },
478
+ {
479
+ kind: "component",
480
+ type: BsGridComponent,
481
+ selector: "bs-grid",
482
+ inputs: ["stopFullWidthAt"]
483
+ },
484
+ {
485
+ kind: "directive",
486
+ type: BsGridRowDirective,
487
+ selector: "[bsRow]"
488
+ },
489
+ {
490
+ kind: "directive",
491
+ type: BsGridColumnDirective,
492
+ selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]",
493
+ inputs: [
494
+ "xxs",
495
+ "xs",
496
+ "sm",
497
+ "md",
498
+ "lg",
499
+ "xl",
500
+ "xxl"
501
+ ]
502
+ },
503
+ {
504
+ kind: "component",
505
+ type: BsPriorityNavComponent,
506
+ selector: "bs-priority-nav",
507
+ inputs: [
508
+ "moreLabel",
509
+ "moreLabelTemplate",
510
+ "collapseAt",
511
+ "overflowFrom",
512
+ "hideEmptyMore",
513
+ "ariaLabel"
514
+ ],
515
+ outputs: ["overflowChange"]
516
+ },
517
+ {
518
+ kind: "directive",
519
+ type: BsPriorityNavItemDirective,
520
+ selector: "[bsPriorityNavItem]",
521
+ inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"]
522
+ },
523
+ {
524
+ kind: "component",
525
+ type: BsTableComponent,
526
+ selector: "bs-table",
527
+ inputs: [
528
+ "isResponsive",
529
+ "striped",
530
+ "hover",
531
+ "border",
532
+ "ariaRowCount"
533
+ ]
534
+ },
535
+ {
536
+ kind: "component",
537
+ type: BsTabControlComponent,
538
+ selector: "bs-tab-control",
539
+ inputs: [
540
+ "border",
541
+ "selectFirstTab",
542
+ "tabsPosition"
543
+ ]
544
+ },
545
+ {
546
+ kind: "component",
547
+ type: BsTabPageComponent,
548
+ selector: "bs-tab-page",
549
+ inputs: ["disabled"]
550
+ },
551
+ {
552
+ kind: "directive",
553
+ type: BsTabPageHeaderDirective,
554
+ selector: "[bsTabPageHeader]"
555
+ },
556
+ {
557
+ kind: "component",
558
+ type: BsSpinnerComponent,
559
+ selector: "bs-spinner",
560
+ inputs: ["type", "color"]
561
+ },
562
+ {
563
+ kind: "component",
564
+ type: SparkIconComponent,
565
+ selector: "spark-icon",
566
+ inputs: ["name"]
567
+ },
568
+ {
569
+ kind: "component",
570
+ type: SparkQueryCardComponent,
571
+ selector: "spark-query-card",
572
+ inputs: [
573
+ "queryId",
574
+ "parentId",
575
+ "parentType",
576
+ "reloadToken",
577
+ "data",
578
+ "search",
579
+ "searchable",
580
+ "selectionMode",
581
+ "deleted",
582
+ "parentDeleted",
583
+ "iconTemplate",
584
+ "captionTemplate",
585
+ "actionsTemplate",
586
+ "rowRoute"
587
+ ],
588
+ outputs: [
589
+ "error",
590
+ "rowClicked",
591
+ "customActionExecuted"
592
+ ]
593
+ },
594
+ {
595
+ kind: "component",
596
+ type: SparkGridCellComponent,
597
+ selector: "spark-grid-cell",
598
+ inputs: [
599
+ "column",
600
+ "display",
601
+ "rendererValue",
602
+ "item",
603
+ "chips",
604
+ "link"
605
+ ]
606
+ },
607
+ {
608
+ kind: "component",
609
+ type: SparkAttributeDescriptionComponent,
610
+ selector: "spark-attribute-description",
611
+ inputs: ["description", "position"]
612
+ },
613
+ {
614
+ kind: "pipe",
615
+ type: i1.DatePipe,
616
+ name: "date"
617
+ },
618
+ {
619
+ kind: "pipe",
620
+ type: ResolveTranslationPipe,
621
+ name: "resolveTranslation"
622
+ },
623
+ {
624
+ kind: "pipe",
625
+ type: TranslateKeyPipe,
626
+ name: "t"
627
+ },
628
+ {
629
+ kind: "pipe",
630
+ type: AttributeValuePipe,
631
+ name: "attributeValue"
632
+ },
633
+ {
634
+ kind: "pipe",
635
+ type: RawAttributeValuePipe,
636
+ name: "rawAttributeValue"
637
+ },
638
+ {
639
+ kind: "pipe",
640
+ type: AsDetailColumnsPipe,
641
+ name: "asDetailColumns"
642
+ },
643
+ {
644
+ kind: "pipe",
645
+ type: AsDetailCellValuePipe,
646
+ name: "asDetailCellValue"
647
+ },
648
+ {
649
+ kind: "pipe",
650
+ type: ArrayValuePipe,
651
+ name: "arrayValue"
652
+ },
653
+ {
654
+ kind: "pipe",
655
+ type: ReferenceLinkRoutePipe,
656
+ name: "referenceLinkRoute"
657
+ },
658
+ {
659
+ kind: "pipe",
660
+ type: ReferenceChipsPipe,
661
+ name: "referenceChips"
662
+ },
663
+ {
664
+ kind: "pipe",
665
+ type: ParsedDatePipe,
666
+ name: "parsedDate"
667
+ }
668
+ ],
669
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
670
+ });
671
+ };
672
+ i0.ɵɵngDeclareClassMetadata({
673
+ minVersion: "12.0.0",
674
+ version: "22.2.0",
675
+ ngImport: i0,
676
+ type: SparkPoDetailComponent,
677
+ decorators: [{
678
+ type: Component,
679
+ args: [{
680
+ selector: "spark-po-detail",
681
+ imports: [
682
+ CommonModule,
683
+ NgTemplateOutlet,
684
+ NgComponentOutlet,
685
+ RouterModule,
686
+ BsAlertComponent,
687
+ BsBadgeComponent,
688
+ BsCardComponent,
689
+ BsCardHeaderComponent,
690
+ BsContainerComponent,
691
+ BsGridComponent,
692
+ BsGridRowDirective,
693
+ BsGridColumnDirective,
694
+ BsPriorityNavComponent,
695
+ BsPriorityNavItemDirective,
696
+ BsTableComponent,
697
+ BsTabControlComponent,
698
+ BsTabPageComponent,
699
+ BsTabPageHeaderDirective,
700
+ BsSpinnerComponent,
701
+ SparkIconComponent,
702
+ SparkQueryCardComponent,
703
+ SparkGridCellComponent,
704
+ ResolveTranslationPipe,
705
+ TranslateKeyPipe,
706
+ AttributeValuePipe,
707
+ RawAttributeValuePipe,
708
+ AsDetailColumnsPipe,
709
+ AsDetailCellValuePipe,
710
+ ArrayValuePipe,
711
+ ReferenceLinkRoutePipe,
712
+ ReferenceChipsPipe,
713
+ ParsedDatePipe,
714
+ SparkAttributeDescriptionComponent
715
+ ],
716
+ changeDetection: ChangeDetectionStrategy.OnPush,
717
+ template: "<bs-container>\n<div class=\"container\">\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n } @else if (item(); as currentItem) {\n @if (entityType(); as et) {\n <div class=\"spark-actionbar py-2\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <button *bsPriorityNavItem=\"1\" class=\"btn btn-outline-secondary\" (click)=\"onBack()\">\n <spark-icon name=\"arrow-left\" /> {{ 'common.back' | t }}\n </button>\n @if (canEdit()) {\n <button *bsPriorityNavItem=\"2\" class=\"btn btn-primary\" (click)=\"onEdit()\">\n <spark-icon name=\"pencil\" /> {{ 'common.edit' | t }}\n </button>\n }\n @if (canDelete()) {\n <button *bsPriorityNavItem=\"3\" class=\"btn btn-danger\" (click)=\"onDelete()\">\n <spark-icon name=\"trash\" /> {{ 'common.delete' | t }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of visibleCustomActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [disabled]=\"runningAction() !== null\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_DETAIL_ACTIONS): each decides for itself whether it applies. -->\n @if (detailContext(); as ctx) {\n @for (action of detailActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n <h2>\n {{ currentItem.breadcrumb || currentItem.name }}\n @if (isDeletedView()) {\n <bs-badge [type]=\"colors.secondary\" class=\"align-middle ms-2 spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"detailTabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #detailTabContent let-tab>\n @if (tab.id === '__default__') {\n @let ungroupedAttrs = ungroupedAttributes();\n @if (ungroupedAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of ungroupedAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n <dl bsRow>\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"detailAttrField; context: { $implicit: attr, item: currentItem }\"></ng-container>\n }\n </dl>\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #detailAttrField let-attr let-currentItem=\"item\">\n <dt [sm]=\"3\">{{ (attr.label | resolveTranslation) || attr.name }}<spark-attribute-description [description]=\"attr.description\" /></dt>\n <dd [sm]=\"9\">\n @if (getDetailRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getDetailRendererInputs(rendererType, attr, currentItem)\"></ng-container>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ (col.label | resolveTranslation) || col.name }}<spark-attribute-description [description]=\"col.description\" /></th>\n }\n </tr>\n </thead>\n <tbody class=\"align-middle\">\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n <!--\n Shared with the query grid. This cell used to hand-roll the renderer\n dispatch and print everything else as text, so a `boolean` column read\n \"true\" here and rendered a checkbox in a grid, and a `color` showed its\n hex. Only the VALUE is resolved locally: these rows are plain\n dictionaries, not PersistentObjects.\n\n `chips` is left unset for a reference array on purpose — an AsDetail\n row's `__sparkBreadcrumbs` is keyed by column, not by id, so there are\n no labels for the members. Text beats a row of raw ids.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(row | asDetailCellValue:attr:col:asDetailReferenceOptions())\"\n [rendererValue]=\"row[col.name]\"\n [item]=\"row\"\n [link]=\"col.dataType === 'Reference' && col.referenceType\n ? (col.referenceType | referenceLinkRoute:row[col.name]:allEntityTypes())\n : null\" />\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length\" class=\"text-center text-muted\">\n {{ 'common.noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\"\n class=\"opacity-100\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"spark-color-swatch d-inline-block align-middle border rounded me-2\" [style.background-color]=\"colorVal\"></span>\n {{ colorVal }}\n } @else {\n -\n }\n } @else if (attr.dataType === 'image') {\n @let imageSrc = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (imageSrc) {\n <img [src]=\"imageSrc\" [alt]=\"(attr.label | resolveTranslation) || attr.name\" loading=\"lazy\"\n class=\"img-fluid rounded\" style=\"max-height: 12rem;\">\n } @else {\n -\n }\n } @else if (attr.dataType === 'url') {\n @let hrefVal = (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes());\n @if (hrefVal) {\n <a [href]=\"hrefVal\" target=\"_blank\" rel=\"noopener noreferrer\">{{ hrefVal }}</a>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n @let chips = (attr.name | referenceChips:item());\n @if (chips.length) {\n <div class=\"d-flex flex-wrap gap-1\">\n @for (chip of chips; track chip.id) {\n @let chipRoute = (attr.referenceType ? (attr.referenceType | referenceLinkRoute:chip.id:allEntityTypes()) : null);\n @if (chipRoute) {\n <a class=\"spark-chip\" [routerLink]=\"chipRoute\">{{ chip.label }}</a>\n } @else {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n }\n </div>\n } @else {\n -\n }\n } @else if (attr.dataType === 'Reference' && attr.referenceType) {\n @let refRoute = (attr.referenceType | referenceLinkRoute:(attr.name | rawAttributeValue:item()):allEntityTypes());\n @if (refRoute && (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes())) {\n <a [routerLink]=\"refRoute\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) }}</a>\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else if (attr.dataType === 'MultiLineString') {\n <div class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}</div>\n } @else if (attr.dataType === 'date' || attr.dataType === 'datetime') {\n <!--\n Formatted the same way, and in the same timezone, as the grid renders it.\n\n Before this, a detail page printed the raw wire value — `2026-12-31T23:59:00-08:00`\n in a definition list — while the grid showed `01/01/2027, 08:59` for the same\n document. Not a rounding difference: a DateTimeOffset whose offset differs from the\n viewer's can disagree on the DATE, so the two pages named different days for one car.\n\n The grid's meaning wins because it is the one a reader can act on: a timestamp is an\n instant, shown in the viewer's own zone. The originating offset is still on the wire\n for code that needs it — server-side logic, exports, \"which country's morning was\n this?\" — it simply is not what a viewer is shown by default.\n -->\n @let when = (attr.name | rawAttributeValue:item()) | parsedDate;\n @if (when) {\n {{ when | date:(attr.dataType === 'date' ? 'mediumDate' : 'short') }}\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) || '-' }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n @for (subQuery of subQueries(); track subQuery.query) {\n <!-- The entry's selectionMode overrides the query's own for this parent (#460, D17). -->\n <spark-query-card [queryId]=\"subQuery.query\" [parentId]=\"currentItem.id!\" [parentType]=\"et.name\"\n [selectionMode]=\"subQuery.selectionMode ?? null\"\n [parentDeleted]=\"isDeletedView()\"\n [iconTemplate]=\"queryIconTemplate()\" [captionTemplate]=\"queryCaptionTemplate()\"\n [actionsTemplate]=\"queryActionsTemplate()\" />\n }\n @if (extraContentTemplate(); as extraContentTpl) {\n <ng-container *ngTemplateOutlet=\"extraContentTpl; context: { $implicit: currentItem, entityType: et }\"></ng-container>\n }\n <!-- Add-on panels (SPARK_DETAIL_PANELS), e.g. <spark-po-history>. -->\n @if (detailContext(); as ctx) {\n @for (panel of detailPanels; track panel.id) {\n <div class=\"spark-detail-panel m-3\">\n <ng-container *ngComponentOutlet=\"panel.component; inputs: { context: ctx }\" />\n </div>\n }\n }\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
718
+ styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}.spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}.container{--spark-content-inset-inline: 0px}.spark-multiline-value{white-space:pre-wrap}\n"]
719
+ }]
720
+ }],
721
+ ctorParameters: () => [],
722
+ propDecorators: {
723
+ showCustomActions: [{
724
+ type: i0.Input,
725
+ args: [{
726
+ isSignal: true,
727
+ alias: "showCustomActions",
728
+ required: false
729
+ }]
730
+ }],
731
+ extraActionsTemplate: [{
732
+ type: i0.Input,
733
+ args: [{
734
+ isSignal: true,
735
+ alias: "extraActionsTemplate",
736
+ required: false
737
+ }]
738
+ }],
739
+ extraContentTemplate: [{
740
+ type: i0.Input,
741
+ args: [{
742
+ isSignal: true,
743
+ alias: "extraContentTemplate",
744
+ required: false
745
+ }]
746
+ }],
747
+ queryIconTemplate: [{
748
+ type: i0.Input,
749
+ args: [{
750
+ isSignal: true,
751
+ alias: "queryIconTemplate",
752
+ required: false
753
+ }]
754
+ }],
755
+ queryCaptionTemplate: [{
756
+ type: i0.Input,
757
+ args: [{
758
+ isSignal: true,
759
+ alias: "queryCaptionTemplate",
760
+ required: false
761
+ }]
762
+ }],
763
+ queryActionsTemplate: [{
764
+ type: i0.Input,
765
+ args: [{
766
+ isSignal: true,
767
+ alias: "queryActionsTemplate",
768
+ required: false
769
+ }]
770
+ }],
771
+ edited: [{
772
+ type: i0.Output,
773
+ args: ["edited"]
774
+ }],
775
+ deleted: [{
776
+ type: i0.Output,
777
+ args: ["deleted"]
778
+ }],
779
+ customActionExecuted: [{
780
+ type: i0.Output,
781
+ args: ["customActionExecuted"]
782
+ }]
783
+ }
784
+ });
410
785
  export { SparkPoDetailComponent };
411
- //# sourceMappingURL=mintplayer-ng-spark-po-detail.mjs.map
786
+
787
+ //# sourceMappingURL=mintplayer-ng-spark-po-detail.mjs.map