@mintplayer/ng-spark 22.26.0 → 22.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/fesm2022/mintplayer-ng-spark-grid.mjs +73 -50
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +19 -10
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +5 -2
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +16 -7
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +30 -14
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +45 -15
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +4 -4
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +18 -2
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +2 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +0 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +12 -9
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +4 -3
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -1
- package/package.json +3 -3
- package/types/mintplayer-ng-spark-grid.d.ts +50 -46
- package/types/mintplayer-ng-spark-models.d.ts +75 -40
- package/types/mintplayer-ng-spark-moderation.d.ts +8 -4
- package/types/mintplayer-ng-spark-pipes.d.ts +3 -0
- package/types/mintplayer-ng-spark-po-create.d.ts +4 -0
- package/types/mintplayer-ng-spark-po-detail.d.ts +12 -2
- package/types/mintplayer-ng-spark-po-edit.d.ts +16 -0
- package/types/mintplayer-ng-spark-po-form.d.ts +1 -0
- package/types/mintplayer-ng-spark-query-list.d.ts +11 -1
- package/types/mintplayer-ng-spark-services.d.ts +38 -9
- package/types/mintplayer-ng-spark-soft-delete.d.ts +1 -1
|
@@ -10,7 +10,7 @@ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
|
|
|
10
10
|
import { SparkService } from "@mintplayer/ng-spark/services";
|
|
11
11
|
import { SparkPoFormComponent } from "@mintplayer/ng-spark/po-form";
|
|
12
12
|
import { ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
|
|
13
|
-
import { ShowedOn, applyOverlay, dictToNestedPo, fromDateInputValue, hasShowedOnFlag, isDateDataType, toDateInputValue } from "@mintplayer/ng-spark/models";
|
|
13
|
+
import { ShowedOn, applyOverlay, dictToNestedPo, fromDateInputValue, hasShowedOnFlag, isDateDataType, overlayFromResponse, toDateInputValue } from "@mintplayer/ng-spark/models";
|
|
14
14
|
var SparkPoCreateComponent = class SparkPoCreateComponent {
|
|
15
15
|
route = inject(ActivatedRoute);
|
|
16
16
|
router = inject(Router);
|
|
@@ -25,7 +25,11 @@ var SparkPoCreateComponent = class SparkPoCreateComponent {
|
|
|
25
25
|
validationErrors = signal([], ...ngDevMode ? [{ debugName: "validationErrors" }] : /* istanbul ignore next */ []);
|
|
26
26
|
isSaving = signal(false, ...ngDevMode ? [{ debugName: "isSaving" }] : /* istanbul ignore next */ []);
|
|
27
27
|
allEntityTypes = signal([], ...ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []);
|
|
28
|
-
generalErrors = computed(() =>
|
|
28
|
+
generalErrors = computed(() => {
|
|
29
|
+
const overlay = this.refreshOverlay();
|
|
30
|
+
const drawn = new Set((this.entityType()?.attributes ?? []).map((a) => applyOverlay(a, overlay[a.name])).filter((a) => hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject)).map((a) => a.name));
|
|
31
|
+
return this.validationErrors().filter((e) => !e.attributeName || !drawn.has(e.attributeName.split(/[.[]/)[0]));
|
|
32
|
+
}, ...ngDevMode ? [{ debugName: "generalErrors" }] : /* istanbul ignore next */ []);
|
|
29
33
|
subQueryParent = signal(null, ...ngDevMode ? [{ debugName: "subQueryParent" }] : /* istanbul ignore next */ []);
|
|
30
34
|
constructor() {
|
|
31
35
|
this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => this.onParamsChange(params));
|
|
@@ -33,7 +37,8 @@ var SparkPoCreateComponent = class SparkPoCreateComponent {
|
|
|
33
37
|
async onParamsChange(params) {
|
|
34
38
|
this.type.set(params.get("type") || "");
|
|
35
39
|
const types = await this.sparkService.getEntityTypes();
|
|
36
|
-
const
|
|
40
|
+
const listed = types.find((t) => t.id === this.type() || t.alias === this.type()) || null;
|
|
41
|
+
const entityType = listed ? await this.sparkService.getEntityType(listed.id, "new") : null;
|
|
37
42
|
this.allEntityTypes.set(types);
|
|
38
43
|
this.initFormData(entityType);
|
|
39
44
|
if (entityType) await this.applyServerDefaults(entityType);
|
|
@@ -52,6 +57,10 @@ var SparkPoCreateComponent = class SparkPoCreateComponent {
|
|
|
52
57
|
this.subQueryParent.set(subQueryParent);
|
|
53
58
|
try {
|
|
54
59
|
const po = await this.sparkService.newObject(this.type(), subQueryParent ?? void 0);
|
|
60
|
+
if (po) {
|
|
61
|
+
this.refreshOverlay.set(overlayFromResponse(po));
|
|
62
|
+
this.initFormData(entityType);
|
|
63
|
+
}
|
|
55
64
|
const editable = new Map(this.getEditableAttributes(entityType).map((a) => [a.name, a]));
|
|
56
65
|
const data = { ...this.formData() };
|
|
57
66
|
for (const attr of po?.attributes ?? []) {
|
|
@@ -82,7 +91,7 @@ var SparkPoCreateComponent = class SparkPoCreateComponent {
|
|
|
82
91
|
}
|
|
83
92
|
getEditableAttributes(entityType = this.entityType()) {
|
|
84
93
|
const overlay = this.refreshOverlay();
|
|
85
|
-
return entityType?.attributes.map((a) => applyOverlay(a, overlay[a.name])).filter((a) =>
|
|
94
|
+
return entityType?.attributes.map((a) => applyOverlay(a, overlay[a.name])).filter((a) => !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject)).sort((a, b) => a.order - b.order) || [];
|
|
86
95
|
}
|
|
87
96
|
async onSave() {
|
|
88
97
|
if (!this.entityType()) return;
|
|
@@ -97,7 +106,6 @@ var SparkPoCreateComponent = class SparkPoCreateComponent {
|
|
|
97
106
|
dataType: attr.dataType,
|
|
98
107
|
isArray: attr.isArray,
|
|
99
108
|
isRequired: attr.isRequired,
|
|
100
|
-
isVisible: attr.isVisible,
|
|
101
109
|
isReadOnly: attr.isReadOnly,
|
|
102
110
|
isValueChanged: true,
|
|
103
111
|
order: attr.order,
|
|
@@ -128,6 +136,7 @@ var SparkPoCreateComponent = class SparkPoCreateComponent {
|
|
|
128
136
|
try {
|
|
129
137
|
const result = await this.sparkService.create(this.type(), po, this.subQueryParent() ?? void 0);
|
|
130
138
|
this.isSaving.set(false);
|
|
139
|
+
if (!result) return;
|
|
131
140
|
this.saved.emit(result);
|
|
132
141
|
this.router.navigate([
|
|
133
142
|
"/po",
|
|
@@ -169,7 +178,7 @@ var SparkPoCreateComponent = class SparkPoCreateComponent {
|
|
|
169
178
|
cancelled: "cancelled"
|
|
170
179
|
},
|
|
171
180
|
ngImport: i0,
|
|
172
|
-
template: "<bs-container>\n<div class=\"container\">\n @if (entityType(); as et) {\n <h2 class=\"mb-4\">{{ 'common.create' | t }} {{ (et.
|
|
181
|
+
template: "<bs-container>\n<div class=\"container\">\n @if (entityType(); as et) {\n <h2 class=\"mb-4\">{{ 'common.create' | t }} {{ (et.label | resolveTranslation) || et.name }}</h2>\n\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n <spark-po-form\n [entityType]=\"et\"\n [(formData)]=\"formData\"\n [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type()\"\n [parentType]=\"type()\"\n [referenceParentId]=\"subQueryParent()?.parentId\"\n [referenceParentType]=\"subQueryParent()?.parentType\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n",
|
|
173
182
|
dependencies: [
|
|
174
183
|
{
|
|
175
184
|
kind: "ngmodule",
|
|
@@ -258,7 +267,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
258
267
|
TranslateKeyPipe
|
|
259
268
|
],
|
|
260
269
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
261
|
-
template: "<bs-container>\n<div class=\"container\">\n @if (entityType(); as et) {\n <h2 class=\"mb-4\">{{ 'common.create' | t }} {{ (et.
|
|
270
|
+
template: "<bs-container>\n<div class=\"container\">\n @if (entityType(); as et) {\n <h2 class=\"mb-4\">{{ 'common.create' | t }} {{ (et.label | resolveTranslation) || et.name }}</h2>\n\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n <spark-po-form\n [entityType]=\"et\"\n [(formData)]=\"formData\"\n [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type()\"\n [parentType]=\"type()\"\n [referenceParentId]=\"subQueryParent()?.parentId\"\n [referenceParentType]=\"subQueryParent()?.parentType\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n"
|
|
262
271
|
}]
|
|
263
272
|
}],
|
|
264
273
|
ctorParameters: () => [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-po-create.mjs","names":[],"sources":["../../po-create/src/spark-po-create.component.ts","../../po-create/src/spark-po-create.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, output, signal } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { CommonModule } from '@angular/common';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SparkService } from '@mintplayer/ng-spark/services';\nimport { SparkPoFormComponent } from '@mintplayer/ng-spark/po-form';\nimport { TranslateKeyPipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport {\n EntityType,\n PersistentObject,\n PersistentObjectAttribute,\n ValidationError,\n ShowedOn,\n hasShowedOnFlag,\n dictToNestedPo,\n EntityTypeResolver,\n isDateDataType,\n fromDateInputValue,\n toDateInputValue,\n RefreshOverlay,\n applyOverlay,\n} from '@mintplayer/ng-spark/models';\n\n@Component({\n selector: 'spark-po-create',\n imports: [CommonModule, BsAlertComponent, BsContainerComponent, BsSpinnerComponent, SparkPoFormComponent, ResolveTranslationPipe, TranslateKeyPipe],\n templateUrl: './spark-po-create.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class SparkPoCreateComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n\n saved = output<PersistentObject>();\n cancelled = output<void>();\n\n colors = Color;\n entityType = signal<EntityType | null>(null);\n type = signal('');\n formData = signal<Record<string, any>>({});\n /** Bound two-way to the form. See {@link getEditableAttributes}. */\n refreshOverlay = signal<RefreshOverlay>({});\n validationErrors = signal<ValidationError[]>([]);\n isSaving = signal(false);\n private allEntityTypes = signal<EntityType[]>([]);\n generalErrors = computed(() => this.validationErrors().filter(e => !e.attributeName));\n /**\n * The parent a sub-query card's New passed in the URL (`parentId`, `parentType`, `queryId`), or\n * null. Forwarded to the form so a Reference attribute's option query runs under the object the\n * New was started from — the same parent `/po/new` receives.\n */\n subQueryParent = signal<{ parentId: string; parentType: string; queryId: string } | null>(null);\n\n constructor() {\n this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => this.onParamsChange(params));\n }\n\n private async onParamsChange(params: any): Promise<void> {\n this.type.set(params.get('type') || '');\n const types = await this.sparkService.getEntityTypes();\n const entityType = types.find(t => t.id === this.type() || t.alias === this.type()) || null;\n this.allEntityTypes.set(types);\n this.initFormData(entityType);\n if (entityType) await this.applyServerDefaults(entityType);\n this.entityType.set(entityType);\n }\n\n /**\n * The blank object comes from the server (#460, D19): `/po/new` runs the type's `OnNewAsync`, so a\n * default a hook sets — and, for a New started from a sub-query, the reference to the parent the\n * base hook fills — reaches the form. The sub-query card passes the parent as query parameters\n * (`parentId`, `parentType`, `queryId`), so it survives the navigation and a reload of this page.\n *\n * A refusal (the parent is gone, or not the caller's to see) is shown and the client-side blank\n * form stays usable, exactly as it was before the round-trip existed.\n */\n private async applyServerDefaults(entityType: EntityType): Promise<void> {\n const query = this.route.snapshot.queryParamMap;\n const parentId = query.get('parentId');\n const parentType = query.get('parentType');\n const queryId = query.get('queryId');\n const subQueryParent = parentId && parentType && queryId ? { parentId, parentType, queryId } : null;\n this.subQueryParent.set(subQueryParent);\n\n try {\n const po = await this.sparkService.newObject(this.type(), subQueryParent ?? undefined);\n const editable = new Map(this.getEditableAttributes(entityType).map(a => [a.name, a] as const));\n const data = { ...this.formData() };\n for (const attr of po?.attributes ?? []) {\n const definition = editable.get(attr.name);\n if (!definition || attr.value === null || attr.value === undefined || definition.dataType === 'AsDetail') continue;\n data[attr.name] = isDateDataType(definition.dataType)\n ? toDateInputValue(definition.dataType, attr.value)\n : attr.value;\n }\n this.formData.set(data);\n } catch (e) {\n const error = e as HttpErrorResponse;\n const errors = error.error?.result?.errors ?? error.error?.errors;\n this.validationErrors.set(errors ?? [{\n attributeName: '',\n errorMessage: { en: error.message || 'The new item could not be prepared.' },\n ruleType: 'error'\n }]);\n }\n }\n\n initFormData(entityType: EntityType | null = this.entityType()): void {\n const data: Record<string, any> = {};\n this.getEditableAttributes(entityType).forEach(attr => {\n if (attr.dataType === 'Reference') {\n data[attr.name] = null;\n } else if (attr.dataType === 'AsDetail') {\n data[attr.name] = attr.isArray ? [] : {};\n } else if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n // Same reason as spark-po-edit: a lookup-backed boolean's third state is the absence of a\n // value, and seeding `false` here would make every new object explicitly opted out.\n data[attr.name] = false;\n } else {\n data[attr.name] = '';\n }\n });\n this.formData.set(data);\n }\n\n /**\n * ⚠️ Overlaid before filtering — same reason as <c>spark-po-edit</c>, and worse here: this list is\n * the <em>only</em> source of the create payload, so an attribute a refresh hook revealed was not\n * merely sent stale, it was absent from the new object entirely.\n */\n getEditableAttributes(entityType: EntityType | null = this.entityType()) {\n const overlay = this.refreshOverlay();\n return entityType?.attributes\n .map(a => applyOverlay(a, overlay[a.name]))\n .filter(a => a.isVisible && !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))\n .sort((a, b) => a.order - b.order) || [];\n }\n\n async onSave(): Promise<void> {\n if (!this.entityType()) return;\n\n this.validationErrors.set([]);\n this.isSaving.set(true);\n\n const resolver: EntityTypeResolver = (clrName) => this.allEntityTypes().find(t => t.clrType === clrName);\n const attributes: PersistentObjectAttribute[] = this.getEditableAttributes().map(attr => {\n const base: PersistentObjectAttribute = {\n id: attr.id,\n name: attr.name,\n // A date control hands back a bare local wall clock; the wire needs a complete ISO-8601\n // instant carrying the viewer's offset for the entered date.\n value: isDateDataType(attr.dataType)\n ? fromDateInputValue(attr.dataType, this.formData()[attr.name])\n : this.formData()[attr.name],\n dataType: attr.dataType,\n isArray: attr.isArray,\n isRequired: attr.isRequired,\n isVisible: attr.isVisible,\n isReadOnly: attr.isReadOnly,\n isValueChanged: true,\n order: attr.order,\n rules: attr.rules,\n };\n\n // AsDetail: pack the flat form dict into nested PO wire shape. Server's polymorphic\n // converter ignores attr.value for AsDetail and reads attr.object / attr.objects.\n if (attr.dataType === 'AsDetail' && attr.asDetailType) {\n const nestedType = resolver(attr.asDetailType);\n if (nestedType) {\n const raw = this.formData()[attr.name];\n base.value = null;\n base.asDetailType = attr.asDetailType;\n if (attr.isArray) {\n const items: any[] = Array.isArray(raw) ? raw : [];\n base.objects = items.map(item => dictToNestedPo((item ?? {}) as Record<string, any>, nestedType, resolver));\n base.object = null;\n } else {\n base.object = raw ? dictToNestedPo(raw as Record<string, any>, nestedType, resolver) : null;\n base.objects = null;\n }\n }\n }\n return base;\n });\n\n const po: Partial<PersistentObject> = {\n name: this.formData()['Name'] || 'New Item',\n objectTypeId: this.entityType()!.id,\n attributes\n };\n\n try {\n const result = await this.sparkService.create(this.type(), po, this.subQueryParent() ?? undefined);\n this.isSaving.set(false);\n this.saved.emit(result);\n this.router.navigate(['/po', this.type(), result.id]);\n } catch (e) {\n this.isSaving.set(false);\n const error = e as HttpErrorResponse;\n // The errors live under `result` (every endpoint wraps its body in a ClientOperationEnvelope:\n // `{ result: { errors: [...] }, operations: [] }`), as in po-edit. Reading only\n // `error.error.errors` showed every create-time refusal as the raw Angular HTTP string.\n const errors = error.error?.result?.errors ?? error.error?.errors;\n if (error.status === 400 && errors) {\n this.validationErrors.set(errors);\n } else {\n this.validationErrors.set([{\n attributeName: '',\n errorMessage: { en: error.message || 'An unexpected error occurred' },\n ruleType: 'error'\n }]);\n }\n }\n }\n\n onCancel(): void {\n this.cancelled.emit();\n window.history.back();\n }\n}\n","<bs-container>\n<div class=\"container\">\n @if (entityType(); as et) {\n <h2 class=\"mb-4\">{{ 'common.create' | t }} {{ (et.description | resolveTranslation) || et.name }}</h2>\n\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n <spark-po-form\n [entityType]=\"et\"\n [(formData)]=\"formData\"\n [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type()\"\n [parentType]=\"type()\"\n [referenceParentId]=\"subQueryParent()?.parentId\"\n [referenceParentType]=\"subQueryParent()?.parentType\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-po-create.mjs","names":[],"sources":["../../po-create/src/spark-po-create.component.ts","../../po-create/src/spark-po-create.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, output, signal } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { CommonModule } from '@angular/common';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SparkService } from '@mintplayer/ng-spark/services';\nimport { SparkPoFormComponent } from '@mintplayer/ng-spark/po-form';\nimport { TranslateKeyPipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport {\n EntityType,\n PersistentObject,\n PersistentObjectAttribute,\n ValidationError,\n ShowedOn,\n hasShowedOnFlag,\n dictToNestedPo,\n EntityTypeResolver,\n isDateDataType,\n fromDateInputValue,\n toDateInputValue,\n RefreshOverlay,\n applyOverlay,\n overlayFromResponse,\n} from '@mintplayer/ng-spark/models';\n\n@Component({\n selector: 'spark-po-create',\n imports: [CommonModule, BsAlertComponent, BsContainerComponent, BsSpinnerComponent, SparkPoFormComponent, ResolveTranslationPipe, TranslateKeyPipe],\n templateUrl: './spark-po-create.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class SparkPoCreateComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n\n saved = output<PersistentObject>();\n cancelled = output<void>();\n\n colors = Color;\n entityType = signal<EntityType | null>(null);\n type = signal('');\n formData = signal<Record<string, any>>({});\n /** Bound two-way to the form. See {@link getEditableAttributes}. */\n refreshOverlay = signal<RefreshOverlay>({});\n validationErrors = signal<ValidationError[]>([]);\n isSaving = signal(false);\n private allEntityTypes = signal<EntityType[]>([]);\n /**\n * The errors the form cannot show next to a field: those with no attribute, and those on an attribute the\n * form does not draw (#264, G6). Those used to be swallowed; Vidyano promotes them to a notification too.\n */\n generalErrors = computed(() => {\n const overlay = this.refreshOverlay();\n const drawn = new Set((this.entityType()?.attributes ?? [])\n .map(a => applyOverlay(a, overlay[a.name]))\n .filter(a => hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))\n .map(a => a.name));\n // A row error names its AsDetail attribute first (\"Jobs[0].Title\", \"Jobs.Title\").\n return this.validationErrors().filter(e => !e.attributeName || !drawn.has(e.attributeName.split(/[.[]/)[0]));\n });\n /**\n * The parent a sub-query card's New passed in the URL (`parentId`, `parentType`, `queryId`), or\n * null. Forwarded to the form so a Reference attribute's option query runs under the object the\n * New was started from — the same parent `/po/new` receives.\n */\n subQueryParent = signal<{ parentId: string; parentType: string; queryId: string } | null>(null);\n\n constructor() {\n this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => this.onParamsChange(params));\n }\n\n private async onParamsChange(params: any): Promise<void> {\n this.type.set(params.get('type') || '');\n const types = await this.sparkService.getEntityTypes();\n const listed = types.find(t => t.id === this.type() || t.alias === this.type()) || null;\n // The create form's own shape (#264, G1/G2): what the caller may not create is absent, and an\n // edit-only deny does not make a field read-only here.\n const entityType = listed ? await this.sparkService.getEntityType(listed.id, 'new') : null;\n this.allEntityTypes.set(types);\n this.initFormData(entityType);\n if (entityType) await this.applyServerDefaults(entityType);\n this.entityType.set(entityType);\n }\n\n /**\n * The blank object comes from the server (#460, D19): `/po/new` runs the type's `OnNewAsync`, so a\n * default a hook sets — and, for a New started from a sub-query, the reference to the parent the\n * base hook fills — reaches the form. The sub-query card passes the parent as query parameters\n * (`parentId`, `parentType`, `queryId`), so it survives the navigation and a reload of this page.\n *\n * A refusal (the parent is gone, or not the caller's to see) is shown and the client-side blank\n * form stays usable, exactly as it was before the round-trip existed.\n */\n private async applyServerDefaults(entityType: EntityType): Promise<void> {\n const query = this.route.snapshot.queryParamMap;\n const parentId = query.get('parentId');\n const parentType = query.get('parentType');\n const queryId = query.get('queryId');\n const subQueryParent = parentId && parentType && queryId ? { parentId, parentType, queryId } : null;\n this.subQueryParent.set(subQueryParent);\n\n try {\n const po = await this.sparkService.newObject(this.type(), subQueryParent ?? undefined);\n // What OnNewAsync decided about the form — the runtime showedOn, isRequired, isReadOnly — applies from\n // the first render, exactly as a refresh's would (#264, G5/G7). Re-initialised, so an attribute the\n // hook revealed gets its slot.\n if (po) {\n this.refreshOverlay.set(overlayFromResponse(po));\n this.initFormData(entityType);\n }\n const editable = new Map(this.getEditableAttributes(entityType).map(a => [a.name, a] as const));\n const data = { ...this.formData() };\n for (const attr of po?.attributes ?? []) {\n const definition = editable.get(attr.name);\n if (!definition || attr.value === null || attr.value === undefined || definition.dataType === 'AsDetail') continue;\n data[attr.name] = isDateDataType(definition.dataType)\n ? toDateInputValue(definition.dataType, attr.value)\n : attr.value;\n }\n this.formData.set(data);\n } catch (e) {\n const error = e as HttpErrorResponse;\n const errors = error.error?.result?.errors ?? error.error?.errors;\n this.validationErrors.set(errors ?? [{\n attributeName: '',\n errorMessage: { en: error.message || 'The new item could not be prepared.' },\n ruleType: 'error'\n }]);\n }\n }\n\n initFormData(entityType: EntityType | null = this.entityType()): void {\n const data: Record<string, any> = {};\n this.getEditableAttributes(entityType).forEach(attr => {\n if (attr.dataType === 'Reference') {\n data[attr.name] = null;\n } else if (attr.dataType === 'AsDetail') {\n data[attr.name] = attr.isArray ? [] : {};\n } else if (attr.dataType === 'boolean' && !attr.lookupReferenceType) {\n // Same reason as spark-po-edit: a lookup-backed boolean's third state is the absence of a\n // value, and seeding `false` here would make every new object explicitly opted out.\n data[attr.name] = false;\n } else {\n data[attr.name] = '';\n }\n });\n this.formData.set(data);\n }\n\n /**\n * ⚠️ Overlaid before filtering — same reason as <c>spark-po-edit</c>, and worse here: this list is\n * the <em>only</em> source of the create payload, so an attribute a refresh hook revealed was not\n * merely sent stale, it was absent from the new object entirely.\n */\n getEditableAttributes(entityType: EntityType | null = this.entityType()) {\n const overlay = this.refreshOverlay();\n return entityType?.attributes\n .map(a => applyOverlay(a, overlay[a.name]))\n .filter(a => !a.isReadOnly && hasShowedOnFlag(a.showedOn, ShowedOn.PersistentObject))\n .sort((a, b) => a.order - b.order) || [];\n }\n\n async onSave(): Promise<void> {\n if (!this.entityType()) return;\n\n this.validationErrors.set([]);\n this.isSaving.set(true);\n\n const resolver: EntityTypeResolver = (clrName) => this.allEntityTypes().find(t => t.clrType === clrName);\n const attributes: PersistentObjectAttribute[] = this.getEditableAttributes().map(attr => {\n const base: PersistentObjectAttribute = {\n id: attr.id,\n name: attr.name,\n // A date control hands back a bare local wall clock; the wire needs a complete ISO-8601\n // instant carrying the viewer's offset for the entered date.\n value: isDateDataType(attr.dataType)\n ? fromDateInputValue(attr.dataType, this.formData()[attr.name])\n : this.formData()[attr.name],\n dataType: attr.dataType,\n isArray: attr.isArray,\n isRequired: attr.isRequired,\n isReadOnly: attr.isReadOnly,\n isValueChanged: true,\n order: attr.order,\n rules: attr.rules,\n };\n\n // AsDetail: pack the flat form dict into nested PO wire shape. Server's polymorphic\n // converter ignores attr.value for AsDetail and reads attr.object / attr.objects.\n if (attr.dataType === 'AsDetail' && attr.asDetailType) {\n const nestedType = resolver(attr.asDetailType);\n if (nestedType) {\n const raw = this.formData()[attr.name];\n base.value = null;\n base.asDetailType = attr.asDetailType;\n if (attr.isArray) {\n const items: any[] = Array.isArray(raw) ? raw : [];\n base.objects = items.map(item => dictToNestedPo((item ?? {}) as Record<string, any>, nestedType, resolver));\n base.object = null;\n } else {\n base.object = raw ? dictToNestedPo(raw as Record<string, any>, nestedType, resolver) : null;\n base.objects = null;\n }\n }\n }\n return base;\n });\n\n const po: Partial<PersistentObject> = {\n name: this.formData()['Name'] || 'New Item',\n objectTypeId: this.entityType()!.id,\n attributes\n };\n\n try {\n const result = await this.sparkService.create(this.type(), po, this.subQueryParent() ?? undefined);\n this.isSaving.set(false);\n // A server interceptor cancelled the create (the user answered its prompt with Cancel): nothing was\n // created, so the form stays as it is.\n if (!result) return;\n this.saved.emit(result);\n this.router.navigate(['/po', this.type(), result.id]);\n } catch (e) {\n this.isSaving.set(false);\n const error = e as HttpErrorResponse;\n // The errors live under `result` (every endpoint wraps its body in a ClientOperationEnvelope:\n // `{ result: { errors: [...] }, operations: [] }`), as in po-edit. Reading only\n // `error.error.errors` showed every create-time refusal as the raw Angular HTTP string.\n const errors = error.error?.result?.errors ?? error.error?.errors;\n if (error.status === 400 && errors) {\n this.validationErrors.set(errors);\n } else {\n this.validationErrors.set([{\n attributeName: '',\n errorMessage: { en: error.message || 'An unexpected error occurred' },\n ruleType: 'error'\n }]);\n }\n }\n }\n\n onCancel(): void {\n this.cancelled.emit();\n window.history.back();\n }\n}\n","<bs-container>\n<div class=\"container\">\n @if (entityType(); as et) {\n <h2 class=\"mb-4\">{{ 'common.create' | t }} {{ (et.label | resolveTranslation) || et.name }}</h2>\n\n @for (error of generalErrors(); track error.errorMessage) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ error.errorMessage | resolveTranslation }}\n </bs-alert>\n }\n\n <spark-po-form\n [entityType]=\"et\"\n [(formData)]=\"formData\"\n [(refreshOverlay)]=\"refreshOverlay\"\n [objectTypeId]=\"type()\"\n [parentType]=\"type()\"\n [referenceParentId]=\"subQueryParent()?.parentId\"\n [referenceParentType]=\"subQueryParent()?.parentType\"\n [validationErrors]=\"validationErrors()\"\n [showButtons]=\"true\"\n [isSaving]=\"isSaving()\"\n (save)=\"onSave()\"\n (cancel)=\"onCancel()\">\n </spark-po-form>\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n"],"mappings":"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"}
|
|
@@ -22,7 +22,7 @@ import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
|
|
|
22
22
|
import { SparkAttributeDescriptionComponent } from "@mintplayer/ng-spark/attribute-description";
|
|
23
23
|
import { SparkGridCellComponent, SparkQueryCardComponent } from "@mintplayer/ng-spark/grid";
|
|
24
24
|
import { SPARK_ATTRIBUTE_RENDERERS, rendererValue, rowRendererInputs, withDeclaredInputs } from "@mintplayer/ng-spark/renderers";
|
|
25
|
-
import { ShowedOn, filterDetailActions, hasShowedOnFlag, subQueriesOf } from "@mintplayer/ng-spark/models";
|
|
25
|
+
import { ShowedOn, builtInAction, confirmationText, filterDetailActions, hasShowedOnFlag, isShowedOnDetail, subQueriesOf } from "@mintplayer/ng-spark/models";
|
|
26
26
|
import { SPARK_DETAIL_ACTIONS, SPARK_DETAIL_PANELS, orderSparkExtensions, parseSparkDeletedParam } from "@mintplayer/ng-spark/panels";
|
|
27
27
|
import { combineLatest } from "rxjs";
|
|
28
28
|
var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
@@ -56,8 +56,10 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
56
56
|
asDetailReferenceOptions = signal({}, ...ngDevMode ? [{ debugName: "asDetailReferenceOptions" }] : /* istanbul ignore next */ []);
|
|
57
57
|
type = "";
|
|
58
58
|
id = "";
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
editAction = signal(null, ...ngDevMode ? [{ debugName: "editAction" }] : /* istanbul ignore next */ []);
|
|
60
|
+
deleteAction = signal(null, ...ngDevMode ? [{ debugName: "deleteAction" }] : /* istanbul ignore next */ []);
|
|
61
|
+
canEdit = computed(() => this.editAction() !== null, ...ngDevMode ? [{ debugName: "canEdit" }] : /* istanbul ignore next */ []);
|
|
62
|
+
canDelete = computed(() => this.deleteAction() !== null, ...ngDevMode ? [{ debugName: "canDelete" }] : /* istanbul ignore next */ []);
|
|
61
63
|
customActions = signal([], ...ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []);
|
|
62
64
|
permissions = signal(null, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
|
|
63
65
|
deletedMode = signal(null, ...ngDevMode ? [{ debugName: "deletedMode" }] : /* istanbul ignore next */ []);
|
|
@@ -150,8 +152,10 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
150
152
|
const withheld = new Set((item.disabledActions ?? []).map((name) => name.toLowerCase()));
|
|
151
153
|
const deletedView = this.isDeletedView();
|
|
152
154
|
this.permissions.set(permissions);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
const edit = builtInAction(actions, "Edit");
|
|
156
|
+
const del = builtInAction(actions, "Delete");
|
|
157
|
+
this.editAction.set(edit && isShowedOnDetail(edit.showedOn) && !deletedView && (can ? can.edit : permissions.canEdit) && !withheld.has("edit") && !withheld.has("save") ? edit : null);
|
|
158
|
+
this.deleteAction.set(del && isShowedOnDetail(del.showedOn) && !deletedView && (can ? can.delete : permissions.canDelete) && !withheld.has("delete") ? del : null);
|
|
155
159
|
this.customActions.set(deletedView ? [] : filterDetailActions(actions));
|
|
156
160
|
}
|
|
157
161
|
} catch (e) {
|
|
@@ -160,7 +164,8 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
160
164
|
}
|
|
161
165
|
}
|
|
162
166
|
visibleAttributes = computed(() => {
|
|
163
|
-
|
|
167
|
+
const loaded = new Map((this.item()?.attributes ?? []).map((a) => [a.name, a]));
|
|
168
|
+
return this.entityType()?.attributes.filter((a) => hasShowedOnFlag(loaded.get(a.name)?.showedOn ?? a.showedOn, ShowedOn.PersistentObject)).sort((a, b) => a.order - b.order) || [];
|
|
164
169
|
}, ...ngDevMode ? [{ debugName: "visibleAttributes" }] : /* istanbul ignore next */ []);
|
|
165
170
|
static DEFAULT_TAB = {
|
|
166
171
|
id: "__default__",
|
|
@@ -266,6 +271,16 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
266
271
|
}
|
|
267
272
|
this.asDetailTypes.set(newAsDetailTypes);
|
|
268
273
|
}
|
|
274
|
+
builtInActionClass(action, fallback) {
|
|
275
|
+
const variant = action.variant?.toLowerCase();
|
|
276
|
+
return [
|
|
277
|
+
"danger",
|
|
278
|
+
"warning",
|
|
279
|
+
"primary",
|
|
280
|
+
"secondary",
|
|
281
|
+
"success"
|
|
282
|
+
].includes(variant ?? "") ? `btn btn-${variant}` : `btn btn-${fallback}`;
|
|
283
|
+
}
|
|
269
284
|
customActionClass(action) {
|
|
270
285
|
const variant = action.variant?.toLowerCase();
|
|
271
286
|
switch (variant) {
|
|
@@ -280,10 +295,8 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
280
295
|
runningAction = signal(null, ...ngDevMode ? [{ debugName: "runningAction" }] : /* istanbul ignore next */ []);
|
|
281
296
|
async onCustomAction(action) {
|
|
282
297
|
if (this.runningAction()) return;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (!confirm(message)) return;
|
|
286
|
-
}
|
|
298
|
+
const message = confirmationText(action, 1);
|
|
299
|
+
if (message && !confirm(message)) return;
|
|
287
300
|
this.runningAction.set(action.name);
|
|
288
301
|
try {
|
|
289
302
|
await this.sparkService.executeCustomAction(this.type, action.name, this.item() || void 0);
|
|
@@ -313,8 +326,11 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
313
326
|
]);
|
|
314
327
|
}
|
|
315
328
|
async onDelete() {
|
|
316
|
-
|
|
317
|
-
|
|
329
|
+
const etag = this.item()?.etag;
|
|
330
|
+
if (!etag) return;
|
|
331
|
+
const message = confirmationText(this.deleteAction(), 1);
|
|
332
|
+
if (!message || confirm(message)) {
|
|
333
|
+
await this.sparkService.delete(this.type, this.id, etag);
|
|
318
334
|
this.deleted.emit();
|
|
319
335
|
await this.returnNavigation.returnToList(this.entityType()?.name);
|
|
320
336
|
}
|
|
@@ -386,7 +402,7 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
|
|
|
386
402
|
customActionExecuted: "customActionExecuted"
|
|
387
403
|
},
|
|
388
404
|
ngImport: i0,
|
|
389
|
-
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():rendererRegistry); 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 @let rowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\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 <!-- A row renderer (rowComponent: contributions' attribution line) draws\n its attributes once, under the row's first cell, instead of as columns. -->\n @if (firstCol && rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row, currentItem)\"></ng-container>\n </div>\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).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",
|
|
405
|
+
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 <!-- Edit and Delete are the composed catalogue's (#467, D8): label, icon and variant included. -->\n @if (editAction(); as edit) {\n <button *bsPriorityNavItem=\"2\" [class]=\"builtInActionClass(edit, 'primary')\" (click)=\"onEdit()\">\n @if (edit.icon) { <spark-icon [name]=\"edit.icon\" /> } {{ edit.label | resolveTranslation }}\n </button>\n }\n @if (deleteAction(); as del) {\n <button *bsPriorityNavItem=\"3\" [class]=\"builtInActionClass(del, 'danger')\" (click)=\"onDelete()\">\n @if (del.icon) { <spark-icon [name]=\"del.icon\" /> } {{ del.label | resolveTranslation }}\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.label | 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():rendererRegistry); 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 @let rowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\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 <!-- A row renderer (rowComponent: contributions' attribution line) draws\n its attributes once, under the row's first cell, instead of as columns. -->\n @if (firstCol && rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row, currentItem)\"></ng-container>\n </div>\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).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",
|
|
390
406
|
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(--spark-shell-main-bg, var(--bs-body-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"],
|
|
391
407
|
dependencies: [
|
|
392
408
|
{
|
|
@@ -723,7 +739,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
723
739
|
SparkAttributeDescriptionComponent
|
|
724
740
|
],
|
|
725
741
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
726
|
-
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():rendererRegistry); 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 @let rowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\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 <!-- A row renderer (rowComponent: contributions' attribution line) draws\n its attributes once, under the row's first cell, instead of as columns. -->\n @if (firstCol && rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row, currentItem)\"></ng-container>\n </div>\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).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",
|
|
742
|
+
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 <!-- Edit and Delete are the composed catalogue's (#467, D8): label, icon and variant included. -->\n @if (editAction(); as edit) {\n <button *bsPriorityNavItem=\"2\" [class]=\"builtInActionClass(edit, 'primary')\" (click)=\"onEdit()\">\n @if (edit.icon) { <spark-icon [name]=\"edit.icon\" /> } {{ edit.label | resolveTranslation }}\n </button>\n }\n @if (deleteAction(); as del) {\n <button *bsPriorityNavItem=\"3\" [class]=\"builtInActionClass(del, 'danger')\" (click)=\"onDelete()\">\n @if (del.icon) { <spark-icon [name]=\"del.icon\" /> } {{ del.label | resolveTranslation }}\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.label | 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():rendererRegistry); 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 @let rowRenderer = (attr | asDetailRowRenderer:asDetailTypes():rendererRegistry);\n @for (row of (attr.name | arrayValue:currentItem); track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes():rendererRegistry); track col.name; let firstCol = $first) {\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 <!-- A row renderer (rowComponent: contributions' attribution line) draws\n its attributes once, under the row's first cell, instead of as columns. -->\n @if (firstCol && rowRenderer) {\n <div class=\"spark-row-renderer\">\n <ng-container *ngComponentOutlet=\"rowRenderer.component; inputs: getRowRendererInputs(rowRenderer, row, currentItem)\"></ng-container>\n </div>\n }\n </td>\n }\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes():rendererRegistry).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",
|
|
727
743
|
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(--spark-shell-main-bg, var(--bs-body-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"]
|
|
728
744
|
}]
|
|
729
745
|
}],
|