@mintplayer/ng-spark 22.25.0 → 22.27.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 (30) hide show
  1. package/README.md +6 -4
  2. package/fesm2022/mintplayer-ng-spark-grid.mjs +71 -44
  3. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-models.mjs +16 -7
  5. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-po-create.mjs +17 -12
  7. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +28 -13
  9. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  10. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +9 -2
  11. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  12. package/fesm2022/mintplayer-ng-spark-po-form.mjs +67 -13
  13. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  14. package/fesm2022/mintplayer-ng-spark-query-list.mjs +18 -2
  15. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +2 -0
  17. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  18. package/fesm2022/mintplayer-ng-spark-services.mjs +12 -9
  19. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  20. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +4 -3
  21. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -1
  22. package/package.json +3 -3
  23. package/types/mintplayer-ng-spark-grid.d.ts +47 -32
  24. package/types/mintplayer-ng-spark-models.d.ts +62 -27
  25. package/types/mintplayer-ng-spark-po-create.d.ts +10 -0
  26. package/types/mintplayer-ng-spark-po-detail.d.ts +8 -2
  27. package/types/mintplayer-ng-spark-po-form.d.ts +20 -0
  28. package/types/mintplayer-ng-spark-query-list.d.ts +11 -1
  29. package/types/mintplayer-ng-spark-services.d.ts +42 -8
  30. package/types/mintplayer-ng-spark-soft-delete.d.ts +1 -1
@@ -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 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') ?? undefined;\n const parentType = query.get('parentType') ?? undefined;\n const queryId = query.get('queryId') ?? undefined;\n const fromSubQuery = !!(parentId && parentType && queryId);\n\n try {\n const po = await this.sparkService.newObject(this.type(), fromSubQuery ? { parentId, parentType, queryId } : 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);\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 [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} 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 // 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
- canEdit = signal(false, ...ngDevMode ? [{ debugName: "canEdit" }] : /* istanbul ignore next */ []);
60
- canDelete = signal(false, ...ngDevMode ? [{ debugName: "canDelete" }] : /* istanbul ignore next */ []);
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
- 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
+ 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) {
@@ -266,6 +270,16 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
266
270
  }
267
271
  this.asDetailTypes.set(newAsDetailTypes);
268
272
  }
273
+ builtInActionClass(action, fallback) {
274
+ const variant = action.variant?.toLowerCase();
275
+ return [
276
+ "danger",
277
+ "warning",
278
+ "primary",
279
+ "secondary",
280
+ "success"
281
+ ].includes(variant ?? "") ? `btn btn-${variant}` : `btn btn-${fallback}`;
282
+ }
269
283
  customActionClass(action) {
270
284
  const variant = action.variant?.toLowerCase();
271
285
  switch (variant) {
@@ -280,10 +294,8 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
280
294
  runningAction = signal(null, ...ngDevMode ? [{ debugName: "runningAction" }] : /* istanbul ignore next */ []);
281
295
  async onCustomAction(action) {
282
296
  if (this.runningAction()) return;
283
- if (action.confirmationMessageKey) {
284
- const message = this.lang.t(action.confirmationMessageKey) || "Are you sure?";
285
- if (!confirm(message)) return;
286
- }
297
+ const message = confirmationText(action, 1);
298
+ if (message && !confirm(message)) return;
287
299
  this.runningAction.set(action.name);
288
300
  try {
289
301
  await this.sparkService.executeCustomAction(this.type, action.name, this.item() || void 0);
@@ -313,8 +325,11 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
313
325
  ]);
314
326
  }
315
327
  async onDelete() {
316
- if (confirm(this.lang.t("common.confirmDelete"))) {
317
- await this.sparkService.delete(this.type, this.id);
328
+ const etag = this.item()?.etag;
329
+ if (!etag) return;
330
+ const message = confirmationText(this.deleteAction(), 1);
331
+ if (!message || confirm(message)) {
332
+ await this.sparkService.delete(this.type, this.id, etag);
318
333
  this.deleted.emit();
319
334
  await this.returnNavigation.returnToList(this.entityType()?.name);
320
335
  }
@@ -386,7 +401,7 @@ var SparkPoDetailComponent = class SparkPoDetailComponent {
386
401
  customActionExecuted: "customActionExecuted"
387
402
  },
388
403
  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",
404
+ 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
405
  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
406
  dependencies: [
392
407
  {
@@ -723,7 +738,7 @@ i0.ɵɵngDeclareClassMetadata({
723
738
  SparkAttributeDescriptionComponent
724
739
  ],
725
740
  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",
741
+ 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
742
  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
743
  }]
729
744
  }],