@mintplayer/ng-spark 22.33.0 → 22.34.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.
@@ -1 +1 @@
1
- {"version":3,"file":"mintplayer-ng-spark-po-detail.mjs","names":[],"sources":["../../po-detail/src/spark-po-detail.component.ts","../../po-detail/src/spark-po-detail.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, TemplateRef, Type, untracked } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { CommonModule, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';\nimport { ActivatedRoute, Router, RouterModule } from '@angular/router';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsBadgeComponent } from '@mintplayer/ng-bootstrap/badge';\nimport { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';\nimport { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';\nimport { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';\nimport { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';\nimport { BsTableComponent } from '@mintplayer/ng-bootstrap/table';\nimport { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } from '@mintplayer/ng-bootstrap/tab-control';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SparkService, SparkLanguageService, SparkLiveService, SparkReturnNavigationService, describeSparkError } from '@mintplayer/ng-spark/services';\nimport { SparkAttributePatch, SparkAttributeRefreshService, SparkClientMethodRegistry, SparkQueryRefreshService } from '@mintplayer/ng-spark/client-operations';\nimport {\n TranslateKeyPipe,\n ResolveTranslationPipe,\n AttributeValuePipe,\n OrDashPipe,\n RawAttributeValuePipe,\n AsDetailColumnsPipe,\n AsDetailCellValuePipe,\n ArrayValuePipe,\n ReferenceLinkRoutePipe,\n ReferenceChipsPipe,\n ParsedDatePipe,\n AsDetailRowRendererPipe,\n} from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkAttributeDescriptionComponent } from '@mintplayer/ng-spark/attribute-description';\nimport { SparkQueryCardComponent, SparkGridCellComponent } from '@mintplayer/ng-spark/grid';\nimport { SPARK_ATTRIBUTE_RENDERERS, SparkResolvedRowRenderer, rendererValue, rowRendererInputs, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';\nimport {\n AttributeGroup,\n AttributeTab,\n CustomActionDefinition,\n EntityAttributeDefinition,\n EntityType,\n LookupReference,\n PersistentObject,\n QueryResultItem,\n ShowedOn,\n hasShowedOnFlag,\n EntityPermissions,\n SparkDeletedFilter,\n builtInAction,\n confirmationText,\n filterDetailActions,\n isShowedOnDetail,\n subQueriesOf,\n} from '@mintplayer/ng-spark/models';\nimport {\n SPARK_DETAIL_ACTIONS,\n SPARK_DETAIL_PANELS,\n SparkDetailContext,\n orderSparkExtensions,\n parseSparkDeletedParam,\n} from '@mintplayer/ng-spark/panels';\nimport { Subscription, combineLatest } from 'rxjs';\n\n@Component({\n selector: 'spark-po-detail',\n imports: [CommonModule, NgTemplateOutlet, NgComponentOutlet, RouterModule, BsAlertComponent, BsBadgeComponent,BsCardComponent, BsCardHeaderComponent, BsContainerComponent, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsPriorityNavComponent, BsPriorityNavItemDirective, BsTableComponent, BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective, BsSpinnerComponent, SparkIconComponent, SparkQueryCardComponent, SparkGridCellComponent, ResolveTranslationPipe, TranslateKeyPipe, AttributeValuePipe, OrDashPipe, RawAttributeValuePipe, AsDetailColumnsPipe, AsDetailRowRendererPipe, AsDetailCellValuePipe, ArrayValuePipe, ReferenceLinkRoutePipe, ReferenceChipsPipe, ParsedDatePipe, SparkAttributeDescriptionComponent],\n templateUrl: './spark-po-detail.component.html',\n styleUrl: './spark-po-detail.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class SparkPoDetailComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n private readonly returnNavigation = inject(SparkReturnNavigationService);\n private readonly queryRefresh = inject(SparkQueryRefreshService);\n private readonly attributeRefresh = inject(SparkAttributeRefreshService);\n private readonly live = inject(SparkLiveService);\n protected readonly lang = inject(SparkLanguageService);\n private readonly clientMethods = inject(SparkClientMethodRegistry);\n protected readonly rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);\n\n /**\n * Add-on panels and action-bar buttons (#460, `SPARK_DETAIL_PANELS` / `SPARK_DETAIL_ACTIONS`).\n * Multi-provided, so History, SoftDelete and Moderation can each add theirs to the page\n * `sparkRoutes()` routes to, which is created by the router and takes no templates.\n */\n protected readonly detailPanels = orderSparkExtensions(inject(SPARK_DETAIL_PANELS, { optional: true }), p => p.order);\n protected readonly detailActions = orderSparkExtensions(inject(SPARK_DETAIL_ACTIONS, { optional: true }), a => a.priority ?? 60);\n\n showCustomActions = input(true);\n extraActionsTemplate = input<TemplateRef<void> | null>(null);\n extraContentTemplate = input<TemplateRef<{ $implicit: PersistentObject; entityType: EntityType }> | null>(null);\n\n /**\n * Header slots forwarded to every sub-query card on this page.\n *\n * A structural directive cannot cross a component boundary, and this component is created by\n * the router — in a default app there is no `<spark-po-detail>` tag to project content into.\n * So an app that wants slotted chrome on its sub-queries substitutes its own route component\n * via `SparkRouteConfig.poDetail` and forwards the templates through here. Forwarding a\n * `TemplateRef` as an input is already this component's idiom; see the two above.\n *\n * Each slot may target one query by alias, so a single forwarded set serves a page rendering\n * several sub-queries.\n */\n queryIconTemplate = input<TemplateRef<any> | null>(null);\n queryCaptionTemplate = input<TemplateRef<any> | null>(null);\n queryActionsTemplate = input<TemplateRef<any> | null>(null);\n\n edited = output<void>();\n deleted = output<void>();\n /** After an action succeeded; `result` is what it handed to `CustomActionArgs.SetResult` (a download url, a new id). */\n customActionExecuted = output<{ action: CustomActionDefinition; item: PersistentObject; result?: unknown }>();\n\n colors = Color;\n /** The page could not be loaded: shown instead of the page. */\n errorMessage = signal<string | null>(null);\n /** A custom action failed or was refused: shown above the page, which stays (it used to replace the page). */\n actionError = signal<string | null>(null);\n entityType = signal<EntityType | null>(null);\n\n /** The sub-queries rendered as cards below the form, in object form (#460, D17). */\n protected readonly subQueries = computed(() => subQueriesOf(this.entityType()));\n allEntityTypes = signal<EntityType[]>([]);\n item = signal<PersistentObject | null>(null);\n lookupReferenceOptions = signal<Record<string, LookupReference>>({});\n asDetailTypes = signal<Record<string, EntityType>>({});\n asDetailReferenceOptions = signal<Record<string, Record<string, QueryResultItem[]>>>({});\n type = '';\n id = '';\n /** The catalogue's Edit (#467, D8) when this page offers it: listed for the caller, on the detail side, and allowed on this row. */\n editAction = signal<CustomActionDefinition | null>(null);\n /** The catalogue's Delete, on the same terms as {@link editAction}. */\n deleteAction = signal<CustomActionDefinition | null>(null);\n canEdit = computed(() => this.editAction() !== null);\n canDelete = computed(() => this.deleteAction() !== null);\n customActions = signal<CustomActionDefinition[]>([]);\n /** Type-level rights, kept for the add-on context; null until loaded. */\n permissions = signal<EntityPermissions | null>(null);\n\n /**\n * The route's `?deleted=` mode (#460, T2): `only` when the row was opened from the recycle bin.\n * Sent with every load of this object; the server honours it only for `ViewDeleted` holders, so\n * for anyone else a deleted row stays a 404.\n */\n deletedMode = signal<SparkDeletedFilter | null>(null);\n\n /** True when the page shows a soft-deleted row: nothing but Restore/Purge applies to it. */\n protected readonly isDeletedView = computed(() => this.deletedMode() === 'only');\n\n /** What every add-on panel and action receives; rebuilt whenever the object or rights change. */\n protected readonly detailContext = computed((): SparkDetailContext | null => {\n const item = this.item();\n const entityType = this.entityType();\n if (!item || !entityType) return null;\n return {\n type: this.type,\n id: this.id,\n item,\n entityType,\n permissions: this.permissions(),\n deleted: this.deletedMode(),\n reload: () => this.load(),\n };\n });\n\n /**\n * The actions actually offered for the object on screen.\n *\n * The catalogue at `/spark/actions/{objectTypeId}` is per TYPE -- the server is never told which\n * row is open -- so an action that applies to only some rows cannot be filtered there. The\n * entity's actions hook decides while it has the entity in hand and withholds what does not\n * apply, and the object arrives carrying that answer.\n *\n * Since #460 (D13) the server asks the same hook again when the action is submitted and refuses a\n * disabled one with 403, so hiding it here matches what the server will do. What this prevents is\n * offering a destructive action where it cannot possibly apply -- Coverage showed an irreversible\n * red \"Delete data\" button on every repository page, healthy ones included, and only admitted it\n * would refuse after the confirmation prompt.\n */\n visibleCustomActions = computed(() => {\n const withheld = this.item()?.disabledActions;\n if (!withheld?.length) return this.customActions();\n\n const lowered = new Set(withheld.map(name => name.toLowerCase()));\n return this.customActions().filter(action => !lowered.has(action.name.toLowerCase()));\n });\n\n constructor() {\n // The query map too: Restore navigates from `?deleted=only` to the same path without it, and the\n // object must then be re-read as a live row.\n combineLatest([this.route.paramMap, this.route.queryParamMap])\n .pipe(takeUntilDestroyed())\n .subscribe(([params, query]) => this.onParamsChange(params, query.get('deleted')));\n\n // Server-issued `refreshAttribute` patches for the object this page is showing.\n //\n // Tracks the token and the entity type, never `item()`: writing the signal this effect reads\n // would re-run it forever, so the read-modify-write of the object happens untracked.\n effect(() => {\n const objectTypeId = this.entityType()?.id;\n if (!this.attributeRefresh.tokenFor(objectTypeId, this.id)) return;\n\n const patches = this.attributeRefresh.patchesFor(objectTypeId, this.id);\n untracked(() => this.applyAttributePatches(patches));\n });\n\n inject(DestroyRef).onDestroy(() => this.stopFollowing());\n }\n\n // --- live (X4) ---------------------------------------------------------------------------------\n\n /** The live subscription of the object on screen, while its type is live; keyed so a reload keeps it. */\n private liveSubscription: Subscription | null = null;\n private liveKey: string | null = null;\n /** The catalogue and rights of the last load, so a pushed object re-derives its Edit/Delete/actions. */\n private loadedActions: CustomActionDefinition[] | null = null;\n\n /**\n * Follows the object when its type is live: every push replaces the shown object and re-derives Edit,\n * Delete and the custom actions from its own `can`/`disabledActions`, as a load does. `gone` (deleted, or\n * no longer readable — one answer) shows the unavailable state. A subscription `error` stops following\n * and keeps the loaded object: the page still shows what the HTTP load returned.\n */\n private follow(et: EntityType | null): void {\n if (!et?.live || !this.id) {\n this.stopFollowing();\n return;\n }\n const deleted = this.deletedMode();\n const key = JSON.stringify([et.id, this.id, deleted]);\n if (key === this.liveKey && this.liveSubscription) return;\n this.stopFollowing();\n this.liveKey = key;\n this.liveSubscription = this.live.watchPersistentObject(et.id, this.id, { deleted }).subscribe(event => {\n if (event.type === 'po') {\n this.item.set(event.po);\n this.deriveActions(event.po);\n } else if (event.type === 'gone') {\n this.liveKey = null;\n this.liveSubscription = null;\n this.item.set(null);\n this.errorMessage.set(this.translateOr('spark.po.unavailable', 'This item was deleted or is no longer available.'));\n } else {\n this.liveKey = null;\n this.liveSubscription = null;\n console.warn(`[spark] live updates stopped for ${et.name} ${this.id}: ${event.code}: ${event.message}`);\n }\n });\n }\n\n private stopFollowing(): void {\n this.liveSubscription?.unsubscribe();\n this.liveSubscription = null;\n this.liveKey = null;\n }\n\n /** The translation, or `fallback` while the key has none (`t` echoes a missing key). */\n private translateOr(key: string, fallback: string): string {\n const text = this.lang.t(key);\n return text && text !== key ? text : fallback;\n }\n\n /**\n * Applies patched attribute values in place, without a re-fetch.\n *\n * The operation carries the value the server computed after its own write, so this is both\n * cheaper and more correct than re-reading: a re-read goes through a RavenDB index that may\n * still be stale from the very write that produced the patch, and would show the old value.\n */\n private applyAttributePatches(patches: Record<string, SparkAttributePatch>): void {\n const current = this.item();\n if (!current) return;\n\n let changed = false;\n const attributes = current.attributes.map(attribute => {\n if (!Object.prototype.hasOwnProperty.call(patches, attribute.name)) return attribute;\n\n const patch = patches[attribute.name];\n const next = { ...attribute };\n let patched = false;\n\n // Each field is applied on its own, because they are not interchangeable: an AsDetail\n // attribute carries its rows in object/objects and leaves value null, so applying only\n // value could never refresh a detail grid.\n //\n // Compared by identity in every case. A scalar skips when unchanged, and a null skips\n // against a null -- which matters because the server writes object/objects as null on a\n // scalar patch, and treating that as a change would repaint on every patch. A rewritten\n // collection is a new array and so never matches, which is the behaviour wanted: comparing\n // rows deeply would cost more than the repaint it saves.\n if (patch.value !== undefined && next.value !== patch.value) {\n next.value = patch.value;\n patched = true;\n }\n // Only for the attributes that actually use them. The server writes object/objects as\n // null on every scalar patch, so applying them unconditionally would mark a scalar\n // attribute changed on each patch and repaint for nothing.\n if (attribute.dataType === 'AsDetail') {\n if (patch.object !== undefined && next.object !== patch.object) {\n next.object = patch.object as PersistentObject | null;\n patched = true;\n }\n if (patch.objects !== undefined && next.objects !== patch.objects) {\n next.objects = patch.objects as PersistentObject[] | null;\n patched = true;\n }\n }\n\n if (!patched) return attribute;\n changed = true;\n return next;\n });\n\n if (changed) this.item.set({ ...current, attributes });\n }\n\n private async onParamsChange(params: any, deleted: string | null = null): Promise<void> {\n this.type = params.get('type') || '';\n this.id = params.get('id') || '';\n // The previous object's pushes must not land on this one, even when this load fails.\n this.stopFollowing();\n const mode = parseSparkDeletedParam(deleted);\n // `exclude` is the default on the wire; keep it out of the request like an absent parameter.\n this.deletedMode.set(mode === 'exclude' ? null : mode);\n await this.load();\n }\n\n /** The object as the page's current mode sees it. Only a set mode is sent. */\n private loadItem(): Promise<PersistentObject> {\n const deleted = this.deletedMode();\n return deleted\n ? this.sparkService.get(this.type, this.id, { deleted })\n : this.sparkService.get(this.type, this.id);\n }\n\n /** (Re)loads the object, its type, rights and actions. Also what add-ons call via `context.reload()`. */\n async load(): Promise<void> {\n this.errorMessage.set(null);\n try {\n const [entityTypes, item] = await Promise.all([\n this.sparkService.getEntityTypes(),\n this.loadItem()\n ]);\n\n const et = entityTypes.find(t => t.id === this.type || t.alias === this.type) || null;\n // Rights and actions are fetched BEFORE the object is published. The component is reused when\n // the route moves to another object, so publishing the new object first would show it under\n // the previous object's Edit/Delete/custom actions until these arrive: a Delete the caller\n // holds on the page they came from flashed on one where they hold none.\n const [permissions, actions] = et\n ? await Promise.all([\n this.sparkService.getPermissions(et.id),\n this.sparkService.getCustomActions(et.id)\n ])\n : [null, null];\n\n this.allEntityTypes.set(entityTypes);\n this.entityType.set(et);\n this.item.set(item);\n this.loadLookupReferenceOptions();\n this.loadAsDetailTypes();\n\n this.permissions.set(et && permissions && actions ? permissions : null);\n this.loadedActions = et && permissions && actions ? actions : null;\n this.deriveActions(item);\n this.follow(et);\n } catch (e) {\n const error = e as HttpErrorResponse;\n this.errorMessage.set(error.error?.error || error.message || 'An unexpected error occurred');\n }\n }\n\n /**\n * Edit, Delete and the custom actions for `item`, from the last load's catalogue and rights: on a load,\n * and again on every live push, since `can` and `disabledActions` travel with the object.\n */\n private deriveActions(item: PersistentObject): void {\n const permissions = this.permissions();\n const actions = this.loadedActions;\n if (!permissions || !actions) {\n this.editAction.set(null);\n this.deleteAction.set(null);\n this.customActions.set([]);\n } else {\n // Prefer the per-row affordances (#236 G5) when the server attached them — a row the\n // caller may read but not mutate then hides its Edit/Delete buttons instead of rendering\n // ones that 404. `can` is absent for types with no row rule, so fall back to type-level.\n // The override (not an intersection) is safe because the server computes `can` as the\n // intersection of type-level rights and the row rule (#243) — it never claims more than\n // `permissions` does.\n const can = item.can;\n // The built-in actions honour `disabledActions` too (#460, D13): the server's\n // OnDisableActionsAsync decides, and refuses a disabled Edit/Save/Delete with 403 at submit,\n // so offering the button would only lead to that refusal. Case-insensitive, like the\n // custom-action filter below.\n const withheld = new Set((item.disabledActions ?? []).map(name => name.toLowerCase()));\n // A row opened from the recycle bin (#460): Edit and Delete judge live rows only — the\n // SoftDelete policy hides a deleted row from both — so they would 404. Restore/Purge come\n // from the soft-delete entry point's detail action instead.\n const deletedView = this.isDeletedView();\n // Edit and Delete come from the composed catalogue (#467, D8): listed only when the caller\n // holds Edit/T or Delete/T, shown here when their showedOn includes the detail side (an app\n // removes one with \"Edit\": null), and the row's `can` must allow it too. The selection rule\n // does not apply: the page acts on the one object it shows.\n const edit = builtInAction(actions, 'Edit');\n const del = builtInAction(actions, 'Delete');\n this.editAction.set(edit && isShowedOnDetail(edit.showedOn) && !deletedView\n && (can ? can.edit : permissions.canEdit) && !withheld.has('edit') && !withheld.has('save') ? edit : null);\n this.deleteAction.set(del && isShowedOnDetail(del.showedOn) && !deletedView\n && (can ? can.delete : permissions.canDelete) && !withheld.has('delete') ? del : null);\n // Custom actions only: the built-ins are rendered above as this page's Edit and Delete.\n this.customActions.set(deletedView ? [] : filterDetailActions(actions));\n }\n }\n\n /**\n * Drawn from the loaded object's own `showedOn`, falling back to the model's (#264, G-Q3): an action sets\n * it per object in `OnLoadAsync` — a stolen car's police report number is shown, the model says `None`.\n */\n visibleAttributes = computed(() => {\n const loaded = new Map((this.item()?.attributes ?? []).map(a => [a.name, a] as const));\n return this.entityType()?.attributes\n .filter(a => hasShowedOnFlag(loaded.get(a.name)?.showedOn ?? a.showedOn, ShowedOn.PersistentObject))\n .sort((a, b) => a.order - b.order) || [];\n });\n\n private static readonly DEFAULT_TAB: AttributeTab = { id: '__default__', name: 'Algemeen', label: { nl: 'Algemeen', en: 'General' }, order: 0 };\n\n ungroupedAttributes = computed(() => {\n const attrs = this.visibleAttributes();\n const groupIds = new Set((this.entityType()?.groups || []).map(g => g.id));\n return attrs.filter(a => !a.group || !groupIds.has(a.group));\n });\n\n resolvedTabs = computed((): AttributeTab[] => {\n const et = this.entityType();\n const definedTabs = et?.tabs?.length ? [...et.tabs].sort((a, b) => a.order - b.order) : [];\n const hasUngroupedAttrs = this.ungroupedAttributes().length > 0;\n const hasUntabbedGroups = (et?.groups || []).some(g => !g.tab);\n\n if (hasUngroupedAttrs || hasUntabbedGroups || definedTabs.length === 0) {\n return [SparkPoDetailComponent.DEFAULT_TAB, ...definedTabs];\n }\n return definedTabs;\n });\n\n groupsForTab(tab: AttributeTab): AttributeGroup[] {\n const groups = this.entityType()?.groups || [];\n if (tab.id === '__default__') {\n return groups.filter(g => !g.tab).sort((a, b) => a.order - b.order);\n }\n return groups.filter(g => g.tab === tab.id).sort((a, b) => a.order - b.order);\n }\n\n attrsForGroup(group: AttributeGroup): EntityAttributeDefinition[] {\n return this.visibleAttributes().filter(a => a.group === group.id);\n }\n\n /** Whether the attribute's renderer asks for the whole row, without a label (`fullWidth`). */\n isFullWidthRenderer(attr: EntityAttributeDefinition): boolean {\n if (!attr.renderer) return false;\n return this.rendererRegistry.find(r => r.name === attr.renderer)?.fullWidth === true;\n }\n\n getDetailRendererComponent(attr: EntityAttributeDefinition): Type<any> | null {\n if (!attr.renderer) return null;\n return this.rendererRegistry.find(r => r.name === attr.renderer)?.detailComponent ?? null;\n }\n\n /** Inputs of an AsDetail row renderer (`rowComponent`): the row and the object that owns it. */\n getRowRendererInputs(renderer: SparkResolvedRowRenderer, row: Record<string, any>, item: PersistentObject): Record<string, any> {\n return rowRendererInputs(renderer, row, item?.id);\n }\n\n getDetailRendererInputs(component: Type<any>, attr: EntityAttributeDefinition, item: PersistentObject): Record<string, any> {\n const itemAttr = item.attributes.find(a => a.name === attr.name);\n const formData: Record<string, any> = {};\n for (const a of item.attributes) {\n formData[a.name] = rendererValue(a);\n }\n return withDeclaredInputs(component, {\n value: rendererValue(itemAttr),\n attribute: attr,\n options: attr.rendererOptions,\n formData,\n item,\n });\n }\n\n /** Column renderer for a cell of an AsDetail sub-table (so embedded rows honor `col.renderer` too). */\n getAsDetailCellRendererComponent(col: EntityAttributeDefinition): Type<any> | null {\n if (!col.renderer) return null;\n return this.rendererRegistry.find(r => r.name === col.renderer)?.columnComponent ?? null;\n }\n\n getAsDetailCellRendererInputs(component: Type<any>, row: Record<string, any>, col: EntityAttributeDefinition): Record<string, any> {\n return withDeclaredInputs(component, {\n value: row[col.name],\n attribute: col,\n options: col.rendererOptions,\n item: row,\n });\n }\n\n private async loadLookupReferenceOptions(): Promise<void> {\n const lookupAttrs = this.visibleAttributes().filter(a => a.lookupReferenceType);\n if (lookupAttrs.length === 0) return;\n\n const lookupNames = [...new Set(lookupAttrs.map(a => a.lookupReferenceType!))];\n // A lookup that fails to load only costs its labels: the attribute then shows its raw value\n // (attributeValue's fallback) instead of the whole page erroring over a secondary request (#453).\n const entries = await Promise.all(\n lookupNames.map(async name => {\n const result = await this.sparkService.getLookupReference(name).catch(() => null);\n return [name, result] as const;\n })\n );\n this.lookupReferenceOptions.set(entries.reduce(\n (acc, [k, v]) => v ? { ...acc, [k]: v } : acc,\n {} as Record<string, LookupReference>));\n }\n\n private async loadAsDetailTypes(): Promise<void> {\n const asDetailAttrs = this.visibleAttributes().filter(a => a.dataType === 'AsDetail' && a.isArray && a.asDetailType);\n if (asDetailAttrs.length === 0) return;\n\n const types = this.allEntityTypes();\n const newAsDetailTypes: Record<string, EntityType> = {};\n // Same fallback as the form: a row type is usually absent from the Query-gated catalogue, and\n // the server now ships its definition alongside the parent (#385).\n const embedded = this.entityType()?.detailTypes ?? [];\n\n for (const attr of asDetailAttrs) {\n const asDetailType =\n types.find(t => t.clrType === attr.asDetailType)\n ?? embedded.find(t => t.clrType === attr.asDetailType);\n if (asDetailType) {\n newAsDetailTypes[attr.name] = asDetailType;\n const refCols = asDetailType.attributes.filter(a => a.dataType === 'Reference' && a.query);\n if (refCols.length > 0) {\n const refEntries = await Promise.all(\n refCols.map(async col => {\n const result = await this.sparkService.executeQueryByName(col.query!, {\n parentId: this.id,\n parentType: this.type,\n });\n return [col.name, result.items] as const;\n })\n );\n this.asDetailReferenceOptions.update(prev => ({\n ...prev,\n [attr.name]: refEntries.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {} as Record<string, QueryResultItem[]>)\n }));\n }\n }\n }\n this.asDetailTypes.set(newAsDetailTypes);\n }\n\n /**\n * The bootstrap classes for a custom action's button. Outline-primary stays the default, so\n * every existing action looks exactly as it did; a server-declared variant opts one action into\n * a louder style. Restricted to a known set rather than interpolated, because the value arrives\n * from a JSON file and must not be able to put arbitrary classes on the button.\n */\n /** Edit's and Delete's button classes: the catalogue's variant, else the look they always had. */\n protected builtInActionClass(action: CustomActionDefinition, fallback: 'primary' | 'danger'): string {\n const variant = action.variant?.toLowerCase();\n return ['danger', 'warning', 'primary', 'secondary', 'success'].includes(variant ?? '')\n ? `btn btn-${variant}`\n : `btn btn-${fallback}`;\n }\n\n protected customActionClass(action: CustomActionDefinition): string {\n const variant = action.variant?.toLowerCase();\n switch (variant) {\n case 'danger':\n case 'warning':\n case 'primary':\n case 'secondary':\n case 'success':\n return `btn btn-${variant}`;\n default:\n return 'btn btn-outline-primary';\n }\n }\n\n /**\n * The action currently running, or null. Drives the disabled state on every custom-action\n * button.\n *\n * A custom action is not necessarily quick: Coverage's Resync makes paged GitHub App calls for\n * every account the caller manages, inside the request. Without this the button stayed live and\n * looked inert for several seconds, and a second click queued a second full run -- which is how\n * a slow action and a broken one became indistinguishable.\n */\n runningAction = signal<string | null>(null);\n\n /**\n * Why an action cannot run in this browser, translated, for its tooltip: the client method it\n * requires (`requiresClient`) is not registered or not supported here (Q9). Null otherwise.\n */\n protected customActionUnavailableReason(action: CustomActionDefinition): string | null {\n const key = this.clientMethods.unavailableReason(action.requiresClient);\n return key === null ? null : this.lang.t(key);\n }\n\n async onCustomAction(action: CustomActionDefinition): Promise<void> {\n // Guard re-entry as well as disabling the button: the template is not the only caller, and a\n // host component driving this method directly would otherwise bypass the check.\n if (this.runningAction()) return;\n if (this.clientMethods.unavailableReason(action.requiresClient) !== null) return;\n\n const message = confirmationText(action, 1);\n if (message && !confirm(message)) return;\n\n this.runningAction.set(action.name);\n this.actionError.set(null);\n try {\n const result = await this.sparkService.executeCustomAction(this.type, action.name, this.item() || undefined);\n this.customActionExecuted.emit({ action, item: this.item()!, result });\n if (action.refreshOnCompleted) {\n // The sub-query grids below do not depend on item(), so re-fetching the PO left them\n // showing pre-action rows -- the action appeared to have done nothing to the very\n // lists it changed.\n //\n // Before the await, not after (#319): a server-issued refreshQuery was already dispatched\n // inside executeCustomAction, and bumps in one synchronous run coalesce into one grid\n // fetch. Bumping after the re-fetch's network round trip landed in a second\n // change-detection flush, and the grid fetched twice for one click.\n for (const subQuery of this.subQueries()) {\n this.queryRefresh.request(subQuery.query);\n }\n\n const item = await this.loadItem();\n this.item.set(item);\n }\n } catch (e) {\n // The envelope's reason (and, for a 429, when to try again), never Angular's HTTP status line.\n this.actionError.set(describeSparkError(e, this.lang, (e as HttpErrorResponse)?.message || this.lang.t('common.actionFailed', 'Action failed')));\n } finally {\n // finally, not after the try: an action that fails must not leave every button on the page\n // permanently disabled.\n this.runningAction.set(null);\n }\n }\n\n onEdit(): void {\n this.edited.emit();\n this.router.navigate(['/po', this.type, this.id, 'edit']);\n }\n\n async onDelete(): Promise<void> {\n // The version on screen is the one deleted (#467, D14): a row changed since is a 409.\n const etag = this.item()?.etag;\n if (!etag) return;\n const message = confirmationText(this.deleteAction(), 1);\n if (!message || confirm(message)) {\n await this.sparkService.delete(this.type, this.id, etag);\n this.deleted.emit();\n // Back to the list the row was opened from (else the type's list), not the start page.\n await this.returnNavigation.returnToList(this.entityType()?.name);\n }\n }\n\n onBack(): void {\n window.history.back();\n }\n}\n","<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 @if (actionError(); as actionErr) {\n <bs-alert [type]=\"colors.danger\" class=\"d-block mt-2 spark-action-error\" role=\"alert\">\n <div class=\"d-flex align-items-start gap-2\">\n <span class=\"flex-grow-1\">{{ actionErr }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-link p-0 text-reset text-decoration-none\" [attr.aria-label]=\"lang.t('common.close', 'Close')\" (click)=\"actionError.set(null)\">&times;</button>\n </div>\n </bs-alert>\n }\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 @let unavailable = customActionUnavailableReason(action);\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [attr.data-action]=\"action.name\"\n [disabled]=\"runningAction() !== null || unavailable !== null\" [attr.title]=\"unavailable\" (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 <!-- A full-width renderer (the core 'paragraph') is body text: no label, the whole row. -->\n @let fullWidth = isFullWidthRenderer(attr);\n @if (!fullWidth) {\n <dt [sm]=\"3\">{{ (attr.label | resolveTranslation) || attr.name }}<spark-attribute-description [description]=\"attr.description\" /></dt>\n }\n <dd [sm]=\"fullWidth ? 12 : 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()) | orDash }}\n }\n } @else if (attr.dataType === 'MultiLineString') {\n <div class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) | orDash }}</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()) | orDash }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) | orDash }}\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"],"mappings":"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"}
1
+ {"version":3,"file":"mintplayer-ng-spark-po-detail.mjs","names":[],"sources":["../../po-detail/src/spark-po-detail.component.ts","../../po-detail/src/spark-po-detail.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, TemplateRef, Type, untracked } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { CommonModule, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';\nimport { ActivatedRoute, Router, RouterModule } from '@angular/router';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsBadgeComponent } from '@mintplayer/ng-bootstrap/badge';\nimport { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';\nimport { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';\nimport { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';\nimport { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';\nimport { BsTableComponent } from '@mintplayer/ng-bootstrap/table';\nimport { BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective } from '@mintplayer/ng-bootstrap/tab-control';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SparkService, SparkLanguageService, SparkLiveService, SparkReturnNavigationService, describeSparkError } from '@mintplayer/ng-spark/services';\nimport { SparkAttributePatch, SparkAttributeRefreshService, SparkClientMethodRegistry, SparkQueryRefreshService } from '@mintplayer/ng-spark/client-operations';\nimport {\n TranslateKeyPipe,\n ResolveTranslationPipe,\n AttributeValuePipe,\n OrDashPipe,\n RawAttributeValuePipe,\n AsDetailColumnsPipe,\n AsDetailCellValuePipe,\n ArrayValuePipe,\n ReferenceLinkRoutePipe,\n ReferenceChipsPipe,\n ParsedDatePipe,\n AsDetailRowRendererPipe,\n} from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkAttributeDescriptionComponent } from '@mintplayer/ng-spark/attribute-description';\nimport { SparkQueryCardComponent, SparkGridCellComponent } from '@mintplayer/ng-spark/grid';\nimport { SPARK_ATTRIBUTE_RENDERERS, SparkResolvedRowRenderer, rendererValue, rowRendererInputs, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';\nimport {\n AttributeGroup,\n AttributeTab,\n CustomActionDefinition,\n EntityAttributeDefinition,\n EntityType,\n LookupReference,\n PersistentObject,\n QueryResultItem,\n ShowedOn,\n hasShowedOnFlag,\n EntityPermissions,\n SparkDeletedFilter,\n builtInAction,\n confirmationText,\n filterDetailActions,\n isShowedOnDetail,\n subQueriesOf,\n} from '@mintplayer/ng-spark/models';\nimport {\n SPARK_DETAIL_ACTIONS,\n SPARK_DETAIL_LAYOUTS,\n SPARK_DETAIL_PANELS,\n SparkDetailContext,\n orderSparkExtensions,\n parseSparkDeletedParam,\n} from '@mintplayer/ng-spark/panels';\nimport { Subscription, combineLatest } from 'rxjs';\n\n@Component({\n selector: 'spark-po-detail',\n imports: [CommonModule, NgTemplateOutlet, NgComponentOutlet, RouterModule, BsAlertComponent, BsBadgeComponent,BsCardComponent, BsCardHeaderComponent, BsContainerComponent, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsPriorityNavComponent, BsPriorityNavItemDirective, BsTableComponent, BsTabControlComponent, BsTabPageComponent, BsTabPageHeaderDirective, BsSpinnerComponent, SparkIconComponent, SparkQueryCardComponent, SparkGridCellComponent, ResolveTranslationPipe, TranslateKeyPipe, AttributeValuePipe, OrDashPipe, RawAttributeValuePipe, AsDetailColumnsPipe, AsDetailRowRendererPipe, AsDetailCellValuePipe, ArrayValuePipe, ReferenceLinkRoutePipe, ReferenceChipsPipe, ParsedDatePipe, SparkAttributeDescriptionComponent],\n templateUrl: './spark-po-detail.component.html',\n styleUrl: './spark-po-detail.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class SparkPoDetailComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n private readonly returnNavigation = inject(SparkReturnNavigationService);\n private readonly queryRefresh = inject(SparkQueryRefreshService);\n private readonly attributeRefresh = inject(SparkAttributeRefreshService);\n private readonly live = inject(SparkLiveService);\n protected readonly lang = inject(SparkLanguageService);\n private readonly clientMethods = inject(SparkClientMethodRegistry);\n protected readonly rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);\n\n /**\n * Add-on panels and action-bar buttons (#460, `SPARK_DETAIL_PANELS` / `SPARK_DETAIL_ACTIONS`).\n * Multi-provided, so History, SoftDelete and Moderation can each add theirs to the page\n * `sparkRoutes()` routes to, which is created by the router and takes no templates.\n */\n protected readonly detailPanels = orderSparkExtensions(inject(SPARK_DETAIL_PANELS, { optional: true }), p => p.order);\n protected readonly detailActions = orderSparkExtensions(inject(SPARK_DETAIL_ACTIONS, { optional: true }), a => a.priority ?? 60);\n private readonly detailLayouts = orderSparkExtensions(inject(SPARK_DETAIL_LAYOUTS, { optional: true }), () => undefined);\n\n showCustomActions = input(true);\n extraActionsTemplate = input<TemplateRef<void> | null>(null);\n extraContentTemplate = input<TemplateRef<{ $implicit: PersistentObject; entityType: EntityType }> | null>(null);\n\n /**\n * Header slots forwarded to every sub-query card on this page.\n *\n * A structural directive cannot cross a component boundary, and this component is created by\n * the router — in a default app there is no `<spark-po-detail>` tag to project content into.\n * So an app that wants slotted chrome on its sub-queries substitutes its own route component\n * via `SparkRouteConfig.poDetail` and forwards the templates through here. Forwarding a\n * `TemplateRef` as an input is already this component's idiom; see the two above.\n *\n * Each slot may target one query by alias, so a single forwarded set serves a page rendering\n * several sub-queries.\n */\n queryIconTemplate = input<TemplateRef<any> | null>(null);\n queryCaptionTemplate = input<TemplateRef<any> | null>(null);\n queryActionsTemplate = input<TemplateRef<any> | null>(null);\n\n edited = output<void>();\n deleted = output<void>();\n /** After an action succeeded; `result` is what it handed to `CustomActionArgs.SetResult` (a download url, a new id). */\n customActionExecuted = output<{ action: CustomActionDefinition; item: PersistentObject; result?: unknown }>();\n\n colors = Color;\n /** The page could not be loaded: shown instead of the page. */\n errorMessage = signal<string | null>(null);\n /** A custom action failed or was refused: shown above the page, which stays (it used to replace the page). */\n actionError = signal<string | null>(null);\n entityType = signal<EntityType | null>(null);\n\n /** The sub-queries rendered as cards below the form, in object form (#460, D17). */\n protected readonly subQueries = computed(() => subQueriesOf(this.entityType()));\n allEntityTypes = signal<EntityType[]>([]);\n item = signal<PersistentObject | null>(null);\n lookupReferenceOptions = signal<Record<string, LookupReference>>({});\n asDetailTypes = signal<Record<string, EntityType>>({});\n asDetailReferenceOptions = signal<Record<string, Record<string, QueryResultItem[]>>>({});\n type = '';\n id = '';\n /** The catalogue's Edit (#467, D8) when this page offers it: listed for the caller, on the detail side, and allowed on this row. */\n editAction = signal<CustomActionDefinition | null>(null);\n /** The catalogue's Delete, on the same terms as {@link editAction}. */\n deleteAction = signal<CustomActionDefinition | null>(null);\n canEdit = computed(() => this.editAction() !== null);\n canDelete = computed(() => this.deleteAction() !== null);\n customActions = signal<CustomActionDefinition[]>([]);\n /** Type-level rights, kept for the add-on context; null until loaded. */\n permissions = signal<EntityPermissions | null>(null);\n\n /**\n * The route's `?deleted=` mode (#460, T2): `only` when the row was opened from the recycle bin.\n * Sent with every load of this object; the server honours it only for `ViewDeleted` holders, so\n * for anyone else a deleted row stays a 404.\n */\n deletedMode = signal<SparkDeletedFilter | null>(null);\n\n /** True when the page shows a soft-deleted row: nothing but Restore/Purge applies to it. */\n protected readonly isDeletedView = computed(() => this.deletedMode() === 'only');\n\n /** What every add-on panel and action receives; rebuilt whenever the object or rights change. */\n protected readonly detailContext = computed((): SparkDetailContext | null => {\n const item = this.item();\n const entityType = this.entityType();\n if (!item || !entityType) return null;\n return {\n type: this.type,\n id: this.id,\n item,\n entityType,\n permissions: this.permissions(),\n deleted: this.deletedMode(),\n reload: () => this.load(),\n };\n });\n\n /** The registered layout that draws this object's attributes instead of the tabs (#504), if one matches. */\n protected readonly detailLayout = computed(() => {\n const item = this.item();\n const entityType = this.entityType();\n if (!item || !entityType) return null;\n return this.detailLayouts.find(l => l.matches(item, entityType)) ?? null;\n });\n\n /**\n * The actions actually offered for the object on screen.\n *\n * The catalogue at `/spark/actions/{objectTypeId}` is per TYPE -- the server is never told which\n * row is open -- so an action that applies to only some rows cannot be filtered there. The\n * entity's actions hook decides while it has the entity in hand and withholds what does not\n * apply, and the object arrives carrying that answer.\n *\n * Since #460 (D13) the server asks the same hook again when the action is submitted and refuses a\n * disabled one with 403, so hiding it here matches what the server will do. What this prevents is\n * offering a destructive action where it cannot possibly apply -- Coverage showed an irreversible\n * red \"Delete data\" button on every repository page, healthy ones included, and only admitted it\n * would refuse after the confirmation prompt.\n */\n visibleCustomActions = computed(() => {\n const withheld = this.item()?.disabledActions;\n if (!withheld?.length) return this.customActions();\n\n const lowered = new Set(withheld.map(name => name.toLowerCase()));\n return this.customActions().filter(action => !lowered.has(action.name.toLowerCase()));\n });\n\n constructor() {\n // The query map too: Restore navigates from `?deleted=only` to the same path without it, and the\n // object must then be re-read as a live row.\n combineLatest([this.route.paramMap, this.route.queryParamMap])\n .pipe(takeUntilDestroyed())\n .subscribe(([params, query]) => this.onParamsChange(params, query.get('deleted')));\n\n // Server-issued `refreshAttribute` patches for the object this page is showing.\n //\n // Tracks the token and the entity type, never `item()`: writing the signal this effect reads\n // would re-run it forever, so the read-modify-write of the object happens untracked.\n effect(() => {\n const objectTypeId = this.entityType()?.id;\n if (!this.attributeRefresh.tokenFor(objectTypeId, this.id)) return;\n\n const patches = this.attributeRefresh.patchesFor(objectTypeId, this.id);\n untracked(() => this.applyAttributePatches(patches));\n });\n\n inject(DestroyRef).onDestroy(() => this.stopFollowing());\n }\n\n // --- live (X4) ---------------------------------------------------------------------------------\n\n /** The live subscription of the object on screen, while its type is live; keyed so a reload keeps it. */\n private liveSubscription: Subscription | null = null;\n private liveKey: string | null = null;\n /** The catalogue and rights of the last load, so a pushed object re-derives its Edit/Delete/actions. */\n private loadedActions: CustomActionDefinition[] | null = null;\n\n /**\n * Follows the object when its type is live: every push replaces the shown object and re-derives Edit,\n * Delete and the custom actions from its own `can`/`disabledActions`, as a load does. `gone` (deleted, or\n * no longer readable — one answer) shows the unavailable state. A subscription `error` stops following\n * and keeps the loaded object: the page still shows what the HTTP load returned.\n */\n private follow(et: EntityType | null): void {\n if (!et?.live || !this.id) {\n this.stopFollowing();\n return;\n }\n const deleted = this.deletedMode();\n const key = JSON.stringify([et.id, this.id, deleted]);\n if (key === this.liveKey && this.liveSubscription) return;\n this.stopFollowing();\n this.liveKey = key;\n this.liveSubscription = this.live.watchPersistentObject(et.id, this.id, { deleted }).subscribe(event => {\n if (event.type === 'po') {\n this.item.set(event.po);\n this.deriveActions(event.po);\n } else if (event.type === 'gone') {\n this.liveKey = null;\n this.liveSubscription = null;\n this.item.set(null);\n this.errorMessage.set(this.translateOr('spark.po.unavailable', 'This item was deleted or is no longer available.'));\n } else {\n this.liveKey = null;\n this.liveSubscription = null;\n console.warn(`[spark] live updates stopped for ${et.name} ${this.id}: ${event.code}: ${event.message}`);\n }\n });\n }\n\n private stopFollowing(): void {\n this.liveSubscription?.unsubscribe();\n this.liveSubscription = null;\n this.liveKey = null;\n }\n\n /** The translation, or `fallback` while the key has none (`t` echoes a missing key). */\n private translateOr(key: string, fallback: string): string {\n const text = this.lang.t(key);\n return text && text !== key ? text : fallback;\n }\n\n /**\n * Applies patched attribute values in place, without a re-fetch.\n *\n * The operation carries the value the server computed after its own write, so this is both\n * cheaper and more correct than re-reading: a re-read goes through a RavenDB index that may\n * still be stale from the very write that produced the patch, and would show the old value.\n */\n private applyAttributePatches(patches: Record<string, SparkAttributePatch>): void {\n const current = this.item();\n if (!current) return;\n\n let changed = false;\n const attributes = current.attributes.map(attribute => {\n if (!Object.prototype.hasOwnProperty.call(patches, attribute.name)) return attribute;\n\n const patch = patches[attribute.name];\n const next = { ...attribute };\n let patched = false;\n\n // Each field is applied on its own, because they are not interchangeable: an AsDetail\n // attribute carries its rows in object/objects and leaves value null, so applying only\n // value could never refresh a detail grid.\n //\n // Compared by identity in every case. A scalar skips when unchanged, and a null skips\n // against a null -- which matters because the server writes object/objects as null on a\n // scalar patch, and treating that as a change would repaint on every patch. A rewritten\n // collection is a new array and so never matches, which is the behaviour wanted: comparing\n // rows deeply would cost more than the repaint it saves.\n if (patch.value !== undefined && next.value !== patch.value) {\n next.value = patch.value;\n patched = true;\n }\n // Only for the attributes that actually use them. The server writes object/objects as\n // null on every scalar patch, so applying them unconditionally would mark a scalar\n // attribute changed on each patch and repaint for nothing.\n if (attribute.dataType === 'AsDetail') {\n if (patch.object !== undefined && next.object !== patch.object) {\n next.object = patch.object as PersistentObject | null;\n patched = true;\n }\n if (patch.objects !== undefined && next.objects !== patch.objects) {\n next.objects = patch.objects as PersistentObject[] | null;\n patched = true;\n }\n }\n\n if (!patched) return attribute;\n changed = true;\n return next;\n });\n\n if (changed) this.item.set({ ...current, attributes });\n }\n\n private async onParamsChange(params: any, deleted: string | null = null): Promise<void> {\n this.type = params.get('type') || '';\n this.id = params.get('id') || '';\n // The previous object's pushes must not land on this one, even when this load fails.\n this.stopFollowing();\n const mode = parseSparkDeletedParam(deleted);\n // `exclude` is the default on the wire; keep it out of the request like an absent parameter.\n this.deletedMode.set(mode === 'exclude' ? null : mode);\n await this.load();\n }\n\n /** The object as the page's current mode sees it. Only a set mode is sent. */\n private loadItem(): Promise<PersistentObject> {\n const deleted = this.deletedMode();\n return deleted\n ? this.sparkService.get(this.type, this.id, { deleted })\n : this.sparkService.get(this.type, this.id);\n }\n\n /** (Re)loads the object, its type, rights and actions. Also what add-ons call via `context.reload()`. */\n async load(): Promise<void> {\n this.errorMessage.set(null);\n try {\n const [entityTypes, item] = await Promise.all([\n this.sparkService.getEntityTypes(),\n this.loadItem()\n ]);\n\n const et = entityTypes.find(t => t.id === this.type || t.alias === this.type) || null;\n // Rights and actions are fetched BEFORE the object is published. The component is reused when\n // the route moves to another object, so publishing the new object first would show it under\n // the previous object's Edit/Delete/custom actions until these arrive: a Delete the caller\n // holds on the page they came from flashed on one where they hold none.\n const [permissions, actions] = et\n ? await Promise.all([\n this.sparkService.getPermissions(et.id),\n this.sparkService.getCustomActions(et.id)\n ])\n : [null, null];\n\n this.allEntityTypes.set(entityTypes);\n this.entityType.set(et);\n this.item.set(item);\n this.loadLookupReferenceOptions();\n this.loadAsDetailTypes();\n\n this.permissions.set(et && permissions && actions ? permissions : null);\n this.loadedActions = et && permissions && actions ? actions : null;\n this.deriveActions(item);\n this.follow(et);\n } catch (e) {\n const error = e as HttpErrorResponse;\n this.errorMessage.set(error.error?.error || error.message || 'An unexpected error occurred');\n }\n }\n\n /**\n * Edit, Delete and the custom actions for `item`, from the last load's catalogue and rights: on a load,\n * and again on every live push, since `can` and `disabledActions` travel with the object.\n */\n private deriveActions(item: PersistentObject): void {\n const permissions = this.permissions();\n const actions = this.loadedActions;\n if (!permissions || !actions) {\n this.editAction.set(null);\n this.deleteAction.set(null);\n this.customActions.set([]);\n } else {\n // Prefer the per-row affordances (#236 G5) when the server attached them — a row the\n // caller may read but not mutate then hides its Edit/Delete buttons instead of rendering\n // ones that 404. `can` is absent for types with no row rule, so fall back to type-level.\n // The override (not an intersection) is safe because the server computes `can` as the\n // intersection of type-level rights and the row rule (#243) — it never claims more than\n // `permissions` does.\n const can = item.can;\n // The built-in actions honour `disabledActions` too (#460, D13): the server's\n // OnDisableActionsAsync decides, and refuses a disabled Edit/Save/Delete with 403 at submit,\n // so offering the button would only lead to that refusal. Case-insensitive, like the\n // custom-action filter below.\n const withheld = new Set((item.disabledActions ?? []).map(name => name.toLowerCase()));\n // A row opened from the recycle bin (#460): Edit and Delete judge live rows only — the\n // SoftDelete policy hides a deleted row from both — so they would 404. Restore/Purge come\n // from the soft-delete entry point's detail action instead.\n const deletedView = this.isDeletedView();\n // Edit and Delete come from the composed catalogue (#467, D8): listed only when the caller\n // holds Edit/T or Delete/T, shown here when their showedOn includes the detail side (an app\n // removes one with \"Edit\": null), and the row's `can` must allow it too. The selection rule\n // does not apply: the page acts on the one object it shows.\n const edit = builtInAction(actions, 'Edit');\n const del = builtInAction(actions, 'Delete');\n this.editAction.set(edit && isShowedOnDetail(edit.showedOn) && !deletedView\n && (can ? can.edit : permissions.canEdit) && !withheld.has('edit') && !withheld.has('save') ? edit : null);\n this.deleteAction.set(del && isShowedOnDetail(del.showedOn) && !deletedView\n && (can ? can.delete : permissions.canDelete) && !withheld.has('delete') ? del : null);\n // Custom actions only: the built-ins are rendered above as this page's Edit and Delete.\n this.customActions.set(deletedView ? [] : filterDetailActions(actions));\n }\n }\n\n /**\n * Drawn from the loaded object's own `showedOn`, falling back to the model's (#264, G-Q3): an action sets\n * it per object in `OnLoadAsync` — a stolen car's police report number is shown, the model says `None`.\n */\n visibleAttributes = computed(() => {\n const loaded = new Map((this.item()?.attributes ?? []).map(a => [a.name, a] as const));\n return this.entityType()?.attributes\n .filter(a => hasShowedOnFlag(loaded.get(a.name)?.showedOn ?? a.showedOn, ShowedOn.PersistentObject))\n .sort((a, b) => a.order - b.order) || [];\n });\n\n private static readonly DEFAULT_TAB: AttributeTab = { id: '__default__', name: 'Algemeen', label: { nl: 'Algemeen', en: 'General' }, order: 0 };\n\n ungroupedAttributes = computed(() => {\n const attrs = this.visibleAttributes();\n const groupIds = new Set((this.entityType()?.groups || []).map(g => g.id));\n return attrs.filter(a => !a.group || !groupIds.has(a.group));\n });\n\n resolvedTabs = computed((): AttributeTab[] => {\n const et = this.entityType();\n const definedTabs = et?.tabs?.length ? [...et.tabs].sort((a, b) => a.order - b.order) : [];\n const hasUngroupedAttrs = this.ungroupedAttributes().length > 0;\n const hasUntabbedGroups = (et?.groups || []).some(g => !g.tab);\n\n if (hasUngroupedAttrs || hasUntabbedGroups || definedTabs.length === 0) {\n return [SparkPoDetailComponent.DEFAULT_TAB, ...definedTabs];\n }\n return definedTabs;\n });\n\n groupsForTab(tab: AttributeTab): AttributeGroup[] {\n const groups = this.entityType()?.groups || [];\n if (tab.id === '__default__') {\n return groups.filter(g => !g.tab).sort((a, b) => a.order - b.order);\n }\n return groups.filter(g => g.tab === tab.id).sort((a, b) => a.order - b.order);\n }\n\n attrsForGroup(group: AttributeGroup): EntityAttributeDefinition[] {\n return this.visibleAttributes().filter(a => a.group === group.id);\n }\n\n /** Whether the attribute's renderer asks for the whole row, without a label (`fullWidth`). */\n isFullWidthRenderer(attr: EntityAttributeDefinition): boolean {\n if (!attr.renderer) return false;\n return this.rendererRegistry.find(r => r.name === attr.renderer)?.fullWidth === true;\n }\n\n getDetailRendererComponent(attr: EntityAttributeDefinition): Type<any> | null {\n if (!attr.renderer) return null;\n return this.rendererRegistry.find(r => r.name === attr.renderer)?.detailComponent ?? null;\n }\n\n /** Inputs of an AsDetail row renderer (`rowComponent`): the row and the object that owns it. */\n getRowRendererInputs(renderer: SparkResolvedRowRenderer, row: Record<string, any>, item: PersistentObject): Record<string, any> {\n return rowRendererInputs(renderer, row, item?.id);\n }\n\n getDetailRendererInputs(component: Type<any>, attr: EntityAttributeDefinition, item: PersistentObject): Record<string, any> {\n const itemAttr = item.attributes.find(a => a.name === attr.name);\n const formData: Record<string, any> = {};\n for (const a of item.attributes) {\n formData[a.name] = rendererValue(a);\n }\n return withDeclaredInputs(component, {\n value: rendererValue(itemAttr),\n attribute: attr,\n options: attr.rendererOptions,\n formData,\n item,\n });\n }\n\n /** Column renderer for a cell of an AsDetail sub-table (so embedded rows honor `col.renderer` too). */\n getAsDetailCellRendererComponent(col: EntityAttributeDefinition): Type<any> | null {\n if (!col.renderer) return null;\n return this.rendererRegistry.find(r => r.name === col.renderer)?.columnComponent ?? null;\n }\n\n getAsDetailCellRendererInputs(component: Type<any>, row: Record<string, any>, col: EntityAttributeDefinition): Record<string, any> {\n return withDeclaredInputs(component, {\n value: row[col.name],\n attribute: col,\n options: col.rendererOptions,\n item: row,\n });\n }\n\n private async loadLookupReferenceOptions(): Promise<void> {\n const lookupAttrs = this.visibleAttributes().filter(a => a.lookupReferenceType);\n if (lookupAttrs.length === 0) return;\n\n const lookupNames = [...new Set(lookupAttrs.map(a => a.lookupReferenceType!))];\n // A lookup that fails to load only costs its labels: the attribute then shows its raw value\n // (attributeValue's fallback) instead of the whole page erroring over a secondary request (#453).\n const entries = await Promise.all(\n lookupNames.map(async name => {\n const result = await this.sparkService.getLookupReference(name).catch(() => null);\n return [name, result] as const;\n })\n );\n this.lookupReferenceOptions.set(entries.reduce(\n (acc, [k, v]) => v ? { ...acc, [k]: v } : acc,\n {} as Record<string, LookupReference>));\n }\n\n private async loadAsDetailTypes(): Promise<void> {\n const asDetailAttrs = this.visibleAttributes().filter(a => a.dataType === 'AsDetail' && a.isArray && a.asDetailType);\n if (asDetailAttrs.length === 0) return;\n\n const types = this.allEntityTypes();\n const newAsDetailTypes: Record<string, EntityType> = {};\n // Same fallback as the form: a row type is usually absent from the Query-gated catalogue, and\n // the server now ships its definition alongside the parent (#385).\n const embedded = this.entityType()?.detailTypes ?? [];\n\n for (const attr of asDetailAttrs) {\n const asDetailType =\n types.find(t => t.clrType === attr.asDetailType)\n ?? embedded.find(t => t.clrType === attr.asDetailType);\n if (asDetailType) {\n newAsDetailTypes[attr.name] = asDetailType;\n const refCols = asDetailType.attributes.filter(a => a.dataType === 'Reference' && a.query);\n if (refCols.length > 0) {\n const refEntries = await Promise.all(\n refCols.map(async col => {\n const result = await this.sparkService.executeQueryByName(col.query!, {\n parentId: this.id,\n parentType: this.type,\n });\n return [col.name, result.items] as const;\n })\n );\n this.asDetailReferenceOptions.update(prev => ({\n ...prev,\n [attr.name]: refEntries.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {} as Record<string, QueryResultItem[]>)\n }));\n }\n }\n }\n this.asDetailTypes.set(newAsDetailTypes);\n }\n\n /**\n * The bootstrap classes for a custom action's button. Outline-primary stays the default, so\n * every existing action looks exactly as it did; a server-declared variant opts one action into\n * a louder style. Restricted to a known set rather than interpolated, because the value arrives\n * from a JSON file and must not be able to put arbitrary classes on the button.\n */\n /** Edit's and Delete's button classes: the catalogue's variant, else the look they always had. */\n protected builtInActionClass(action: CustomActionDefinition, fallback: 'primary' | 'danger'): string {\n const variant = action.variant?.toLowerCase();\n return ['danger', 'warning', 'primary', 'secondary', 'success'].includes(variant ?? '')\n ? `btn btn-${variant}`\n : `btn btn-${fallback}`;\n }\n\n protected customActionClass(action: CustomActionDefinition): string {\n const variant = action.variant?.toLowerCase();\n switch (variant) {\n case 'danger':\n case 'warning':\n case 'primary':\n case 'secondary':\n case 'success':\n return `btn btn-${variant}`;\n default:\n return 'btn btn-outline-primary';\n }\n }\n\n /**\n * The action currently running, or null. Drives the disabled state on every custom-action\n * button.\n *\n * A custom action is not necessarily quick: Coverage's Resync makes paged GitHub App calls for\n * every account the caller manages, inside the request. Without this the button stayed live and\n * looked inert for several seconds, and a second click queued a second full run -- which is how\n * a slow action and a broken one became indistinguishable.\n */\n runningAction = signal<string | null>(null);\n\n /**\n * Why an action cannot run in this browser, translated, for its tooltip: the client method it\n * requires (`requiresClient`) is not registered or not supported here (Q9). Null otherwise.\n */\n protected customActionUnavailableReason(action: CustomActionDefinition): string | null {\n const key = this.clientMethods.unavailableReason(action.requiresClient);\n return key === null ? null : this.lang.t(key);\n }\n\n async onCustomAction(action: CustomActionDefinition): Promise<void> {\n // Guard re-entry as well as disabling the button: the template is not the only caller, and a\n // host component driving this method directly would otherwise bypass the check.\n if (this.runningAction()) return;\n if (this.clientMethods.unavailableReason(action.requiresClient) !== null) return;\n\n const message = confirmationText(action, 1);\n if (message && !confirm(message)) return;\n\n this.runningAction.set(action.name);\n this.actionError.set(null);\n try {\n const result = await this.sparkService.executeCustomAction(this.type, action.name, this.item() || undefined);\n this.customActionExecuted.emit({ action, item: this.item()!, result });\n if (action.refreshOnCompleted) {\n // The sub-query grids below do not depend on item(), so re-fetching the PO left them\n // showing pre-action rows -- the action appeared to have done nothing to the very\n // lists it changed.\n //\n // Before the await, not after (#319): a server-issued refreshQuery was already dispatched\n // inside executeCustomAction, and bumps in one synchronous run coalesce into one grid\n // fetch. Bumping after the re-fetch's network round trip landed in a second\n // change-detection flush, and the grid fetched twice for one click.\n for (const subQuery of this.subQueries()) {\n this.queryRefresh.request(subQuery.query);\n }\n\n const item = await this.loadItem();\n this.item.set(item);\n }\n } catch (e) {\n // The envelope's reason (and, for a 429, when to try again), never Angular's HTTP status line.\n this.actionError.set(describeSparkError(e, this.lang, (e as HttpErrorResponse)?.message || this.lang.t('common.actionFailed', 'Action failed')));\n } finally {\n // finally, not after the try: an action that fails must not leave every button on the page\n // permanently disabled.\n this.runningAction.set(null);\n }\n }\n\n onEdit(): void {\n this.edited.emit();\n this.router.navigate(['/po', this.type, this.id, 'edit']);\n }\n\n async onDelete(): Promise<void> {\n // The version on screen is the one deleted (#467, D14): a row changed since is a 409.\n const etag = this.item()?.etag;\n if (!etag) return;\n const message = confirmationText(this.deleteAction(), 1);\n if (!message || confirm(message)) {\n await this.sparkService.delete(this.type, this.id, etag);\n this.deleted.emit();\n // Back to the list the row was opened from (else the type's list), not the start page.\n await this.returnNavigation.returnToList(this.entityType()?.name);\n }\n }\n\n onBack(): void {\n window.history.back();\n }\n}\n","<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 @if (actionError(); as actionErr) {\n <bs-alert [type]=\"colors.danger\" class=\"d-block mt-2 spark-action-error\" role=\"alert\">\n <div class=\"d-flex align-items-start gap-2\">\n <span class=\"flex-grow-1\">{{ actionErr }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-link p-0 text-reset text-decoration-none\" [attr.aria-label]=\"lang.t('common.close', 'Close')\" (click)=\"actionError.set(null)\">&times;</button>\n </div>\n </bs-alert>\n }\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 @let unavailable = customActionUnavailableReason(action);\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\" [attr.data-action]=\"action.name\"\n [disabled]=\"runningAction() !== null || unavailable !== null\" [attr.title]=\"unavailable\" (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 @if (detailLayout(); as layout) {\n <!-- A registered layout draws the attributes instead (SPARK_DETAIL_LAYOUTS): Dashboards' tiles. -->\n <ng-container *ngComponentOutlet=\"layout.component; inputs: { context: detailContext() }\" />\n } @else {\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 <!-- A full-width renderer (the core 'paragraph') is body text: no label, the whole row. -->\n @let fullWidth = isFullWidthRenderer(attr);\n @if (!fullWidth) {\n <dt [sm]=\"3\">{{ (attr.label | resolveTranslation) || attr.name }}<spark-attribute-description [description]=\"attr.description\" /></dt>\n }\n <dd [sm]=\"fullWidth ? 12 : 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()) | orDash }}\n }\n } @else if (attr.dataType === 'MultiLineString') {\n <div class=\"spark-multiline-value\">{{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) | orDash }}</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()) | orDash }}\n }\n } @else {\n {{ (attr.name | attributeValue:item():entityType():lookupReferenceOptions():allEntityTypes()) | orDash }}\n }\n </dd>\n </ng-template>\n </bs-grid>\n }\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"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mintplayer/ng-spark",
3
3
  "private": false,
4
- "version": "22.33.0",
4
+ "version": "22.34.0",
5
5
  "description": "Angular component library for MintPlayer.Spark CRUD applications",
6
6
  "repository": {
7
7
  "type": "git",
@@ -120,6 +120,10 @@
120
120
  "types": "./types/mintplayer-ng-spark-contributions.d.ts",
121
121
  "default": "./fesm2022/mintplayer-ng-spark-contributions.mjs"
122
122
  },
123
+ "./dashboards": {
124
+ "types": "./types/mintplayer-ng-spark-dashboards.d.ts",
125
+ "default": "./fesm2022/mintplayer-ng-spark-dashboards.mjs"
126
+ },
123
127
  "./grid": {
124
128
  "types": "./types/mintplayer-ng-spark-grid.d.ts",
125
129
  "default": "./fesm2022/mintplayer-ng-spark-grid.mjs"
@@ -0,0 +1,226 @@
1
+ import { SparkAttributeColumnRenderer, SparkAttributeDetailRenderer, SparkAttributeRendererRegistration } from "@mintplayer/ng-spark/renderers";
2
+ import * as i0 from "@angular/core";
3
+ import { Provider, Type } from "@angular/core";
4
+ import { Color } from "@mintplayer/ng-bootstrap";
5
+ import { SparkLanguageService } from "@mintplayer/ng-spark/services";
6
+ import { EntityAttributeDefinition, PersistentObject, TranslatedString } from "@mintplayer/ng-spark/models";
7
+ import { SparkDetailContext } from "@mintplayer/ng-spark/panels";
8
+ /** How a dashboard value is written: a plain number, bytes (binary units), a percentage, a date, or as is. */
9
+ export type SparkDashboardFormat = 'number' | 'bytes' | 'percent' | 'date' | 'datetime' | 'text';
10
+ /** A finite number from a wire value (a number or a numeric string), else null. */
11
+ export declare function toSparkNumber(value: unknown): number | null;
12
+ /** 1536 → "1.5 KB": binary units, one decimal from KB up; "—" for no value. */
13
+ export declare function formatSparkBytes(bytes: number | null | undefined, locale?: string): string;
14
+ /**
15
+ * A value as a dashboard writes it: "—" for none; numbers by `format` (`decimals` places for `number`/`percent`,
16
+ * default 0); dates in the viewer's locale; anything else as text.
17
+ */
18
+ export declare function formatSparkDashboardValue(value: unknown, format: SparkDashboardFormat | undefined, locale?: string, decimals?: number): string;
19
+ /** The colour a threshold gives a value: `danger` at or past `danger`, `warning` at or past `warning`, else none. */
20
+ export declare function sparkThresholdColor(value: number | null, thresholds: {
21
+ warning?: number;
22
+ danger?: number;
23
+ direction?: 'above' | 'below';
24
+ }): 'warning' | 'danger' | null;
25
+ /** `rendererOptions` of the `spark-stat` renderer. Every member is optional. */
26
+ export interface SparkStatOptions {
27
+ /** How the value is written. Default `'number'`. */
28
+ format?: SparkDashboardFormat;
29
+ /** Decimals for `number` (default 0) and `percent` (default 1). */
30
+ decimals?: number;
31
+ /** Text after the value, e.g. `' sites'`. Default none. */
32
+ suffix?: string;
33
+ /** The value turns amber at or past this. */
34
+ warning?: number;
35
+ /** The value turns red at or past this. */
36
+ danger?: number;
37
+ /** `'above'` (default): high is bad, as for disk use. `'below'`: low is bad, as for free space. */
38
+ direction?: 'above' | 'below';
39
+ }
40
+ /**
41
+ * The `spark-stat` renderer: one figure, large on a detail page (a dashboard tile) and plain in a grid cell,
42
+ * coloured amber or red once it passes the model's `warning` / `danger` threshold. No value draws a muted dash.
43
+ */
44
+ export declare class SparkStatComponent implements SparkAttributeColumnRenderer, SparkAttributeDetailRenderer {
45
+ private readonly locale;
46
+ readonly value: import("@angular/core").InputSignal<any>;
47
+ readonly options: import("@angular/core").InputSignal<Record<string, any> | undefined>;
48
+ private readonly settings;
49
+ protected readonly text: import("@angular/core").Signal<string>;
50
+ protected readonly color: import("@angular/core").Signal<"warning" | "danger" | null>;
51
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkStatComponent, never>;
52
+ static ɵcmp: i0.ɵɵComponentDeclaration<SparkStatComponent, "spark-stat", never, {
53
+ "value": {
54
+ "alias": "value";
55
+ "required": false;
56
+ "isSignal": true;
57
+ };
58
+ "options": {
59
+ "alias": "options";
60
+ "required": false;
61
+ "isSignal": true;
62
+ };
63
+ }, {}, never, never, true, never>;
64
+ }
65
+ /** `rendererOptions` of the `spark-meter` renderer. Every member is optional. */
66
+ export interface SparkMeterOptions {
67
+ /** The ceiling when the value is a plain number (a value `{ value, max }` brings its own). Default 100. */
68
+ max?: number;
69
+ /** How the value and the ceiling are written. Default `'number'`. */
70
+ format?: SparkDashboardFormat;
71
+ /** Amber from this share of the ceiling, in percent. Default 75. */
72
+ warning?: number;
73
+ /** Red from this share of the ceiling, in percent. Default 90. */
74
+ danger?: number;
75
+ /** The meter's accessible name. Default `'Usage'`. */
76
+ label?: string;
77
+ }
78
+ interface MeterView {
79
+ value: number;
80
+ max: number;
81
+ /** The share of the ceiling, in percent (0–100), one decimal. */
82
+ share: number;
83
+ color: Color;
84
+ text: string;
85
+ }
86
+ /**
87
+ * The `spark-meter` renderer: how much of something is used, as a bar coloured by its share (green, amber from 75 %,
88
+ * red from 90 % by default) with "used of total (share)" next to it. The value is a number against the `max` option, or
89
+ * `{ value, max }` from the server (the dashboards package's `DashboardValues.Meter`), e.g. disk bytes used of the total.
90
+ * No value, or no ceiling, draws a muted dash.
91
+ */
92
+ export declare class SparkMeterComponent implements SparkAttributeColumnRenderer, SparkAttributeDetailRenderer {
93
+ private readonly locale;
94
+ readonly value: import("@angular/core").InputSignal<any>;
95
+ readonly options: import("@angular/core").InputSignal<Record<string, any> | undefined>;
96
+ private readonly settings;
97
+ protected readonly label: import("@angular/core").Signal<string>;
98
+ protected readonly view: import("@angular/core").Signal<MeterView | null>;
99
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkMeterComponent, never>;
100
+ static ɵcmp: i0.ɵɵComponentDeclaration<SparkMeterComponent, "spark-meter", never, {
101
+ "value": {
102
+ "alias": "value";
103
+ "required": false;
104
+ "isSignal": true;
105
+ };
106
+ "options": {
107
+ "alias": "options";
108
+ "required": false;
109
+ "isSignal": true;
110
+ };
111
+ }, {}, never, never, true, never>;
112
+ }
113
+ /** One column of a `spark-dashboard-table`. */
114
+ export interface SparkDashboardTableColumn {
115
+ /** The row field. For a value that is a dictionary, its rows are `{ key, value }`. */
116
+ key: string;
117
+ /** A translation key for the header (`translations.json`), shown as is when it has no translation. Default `key`. */
118
+ label?: string;
119
+ /** How the cells are written. Numbers and bytes are right-aligned. Default: as is. */
120
+ format?: SparkDashboardFormat | 'boolean';
121
+ /** Translate each cell as `{translate}{value}` (e.g. `'seoUi.runStatus.'`), falling back to the value. */
122
+ translate?: string;
123
+ }
124
+ /** `rendererOptions` of the `spark-dashboard-table` renderer. */
125
+ export interface SparkDashboardTableOptions {
126
+ /** The columns, in order. Default: the first row's fields. */
127
+ columns?: SparkDashboardTableColumn[];
128
+ /** A translation key shown when there are no rows. Default `'dashboards.empty'`. */
129
+ empty?: string;
130
+ }
131
+ interface TableCell {
132
+ text: string;
133
+ check: boolean;
134
+ }
135
+ /**
136
+ * The `spark-dashboard-table` renderer (detail pages): a dashboard value that is a list of rows, such as the
137
+ * RavenDB statistics' collections (D12: tables are JSON attributes). A dictionary (`{ "Completed": 3 }`) reads as
138
+ * rows `{ key, value }`.
139
+ */
140
+ export declare class SparkDashboardTableComponent implements SparkAttributeDetailRenderer {
141
+ private readonly locale;
142
+ protected readonly lang: SparkLanguageService;
143
+ readonly value: import("@angular/core").InputSignal<any>;
144
+ readonly options: import("@angular/core").InputSignal<Record<string, any> | undefined>;
145
+ protected readonly settings: import("@angular/core").Signal<SparkDashboardTableOptions>;
146
+ protected readonly rows: import("@angular/core").Signal<Record<string, unknown>[]>;
147
+ protected readonly columns: import("@angular/core").Signal<SparkDashboardTableColumn[]>;
148
+ protected readonly cells: import("@angular/core").Signal<TableCell[][]>;
149
+ protected header(column: SparkDashboardTableColumn): string;
150
+ protected isNumeric(column: SparkDashboardTableColumn | undefined): boolean;
151
+ private cell;
152
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkDashboardTableComponent, never>;
153
+ static ɵcmp: i0.ɵɵComponentDeclaration<SparkDashboardTableComponent, "spark-dashboard-table", never, {
154
+ "value": {
155
+ "alias": "value";
156
+ "required": false;
157
+ "isSignal": true;
158
+ };
159
+ "options": {
160
+ "alias": "options";
161
+ "required": false;
162
+ "isSignal": true;
163
+ };
164
+ }, {}, never, never, true, never>;
165
+ }
166
+ /** The rows of a table value: a list of objects as is, a dictionary as `{ key, value }` rows, anything else none. */
167
+ export declare function toSparkTableRows(value: unknown): Record<string, unknown>[];
168
+ /** The `metadata` key a dashboard carries (the dashboards package's `SparkDashboardActions.MetadataKey`). */
169
+ export declare const SPARK_DASHBOARD_METADATA_KEY = "dashboard";
170
+ /** Whether `item` is a dashboard: the server marks one with `metadata.dashboard`. */
171
+ export declare function isSparkDashboard(item: PersistentObject | null | undefined): boolean;
172
+ interface DashboardTile {
173
+ attribute: EntityAttributeDefinition;
174
+ /** Columns spanned: 1, 2, or the whole row. */
175
+ span: 1 | 2 | 'row';
176
+ component: Type<any> | null;
177
+ inputs: Record<string, any>;
178
+ /** The value as text, for an attribute without a detail renderer. */
179
+ text: string;
180
+ }
181
+ interface DashboardSection {
182
+ id: string;
183
+ name: string | null;
184
+ label?: TranslatedString;
185
+ tiles: DashboardTile[];
186
+ }
187
+ /**
188
+ * The dashboard tile layout (D12): replaces a dashboard's attribute cards on the routed detail page
189
+ * (`SPARK_DETAIL_LAYOUTS`, registered by `provideSparkDashboards()`). Each group of the model is a section, each
190
+ * attribute a tile: its label above its detail renderer (`spark-stat`, `spark-meter`, `spark-trend`,
191
+ * `spark-dashboard-table`, …) or its plain value. `columnSpan` 2 takes two columns, 3 and more (and a table without one) the
192
+ * whole row. An attribute the caller may not read is not on the object and has no tile. The page follows the object
193
+ * over `/spark/live` like any live type, so the tiles change in place.
194
+ */
195
+ export declare class SparkDashboardComponent {
196
+ private readonly locale;
197
+ private readonly renderers;
198
+ readonly context: import("@angular/core").InputSignal<SparkDetailContext>;
199
+ protected readonly sections: import("@angular/core").Signal<DashboardSection[]>;
200
+ /** When the server read the values, in the viewer's locale. */
201
+ protected readonly generatedAt: import("@angular/core").Signal<string | null>;
202
+ private tile;
203
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkDashboardComponent, never>;
204
+ static ɵcmp: i0.ɵɵComponentDeclaration<SparkDashboardComponent, "spark-dashboard", never, {
205
+ "context": {
206
+ "alias": "context";
207
+ "required": true;
208
+ "isSignal": true;
209
+ };
210
+ }, {}, never, never, true, never>;
211
+ }
212
+ /**
213
+ * The dashboard renderers under their model names. Renderers are one list, so add these to the app's own in its
214
+ * single `provideSparkAttributeRenderers` (or `provideSparkScoreRenderers`, which `spark-trend` comes from) call:
215
+ *
216
+ * @example
217
+ * provideSparkScoreRenderers([...appRenderers, ...sparkDashboardRenderers]),
218
+ * provideSparkDashboards(),
219
+ */
220
+ export declare const sparkDashboardRenderers: readonly SparkAttributeRendererRegistration[];
221
+ /**
222
+ * Draws every dashboard (an object the server marks with `metadata.dashboard`, the dashboards package's live virtual
223
+ * types) with the tile layout instead of the attribute cards, on the routed detail page. The renderers are registered
224
+ * separately: see {@link sparkDashboardRenderers}.
225
+ */
226
+ export declare function provideSparkDashboards(): Provider[];
@@ -119,6 +119,22 @@ export interface SparkQueryRowAction {
119
119
  export declare const SPARK_QUERY_ROW_ACTIONS: InjectionToken<SparkQueryRowAction[]>;
120
120
  /** Registers grid row actions (multi — every call adds). */
121
121
  export declare function provideSparkQueryRowActions(...actions: SparkQueryRowAction[]): Provider[];
122
+ /**
123
+ * A layout that draws a detail page's attributes in place of the tabs and cards (#504), for objects it
124
+ * `matches`: Dashboards' tile layout for objects carrying `metadata.dashboard`. It must declare
125
+ * `context = input.required<SparkDetailContext>()`. The action bar, sub-queries, the extra content
126
+ * template and the detail panels stay; the first matching layout wins.
127
+ */
128
+ export interface SparkDetailLayout {
129
+ id: string;
130
+ component: Type<unknown>;
131
+ /** Whether this layout draws `item`. Called on every change of the object, so keep it cheap. */
132
+ matches(item: PersistentObject, entityType: EntityType): boolean;
133
+ }
134
+ /** Multi-provider: layouts that replace the attribute cards of the routed detail page for the objects they match. */
135
+ export declare const SPARK_DETAIL_LAYOUTS: InjectionToken<SparkDetailLayout[]>;
136
+ /** Registers detail layouts (multi — every call adds). */
137
+ export declare function provideSparkDetailLayouts(...layouts: SparkDetailLayout[]): Provider[];
122
138
  /** Multi-provider: panels under the attributes of the routed detail page. */
123
139
  export declare const SPARK_DETAIL_PANELS: InjectionToken<SparkDetailPanel[]>;
124
140
  /** Multi-provider: extra buttons in the routed detail page's action bar. */
@@ -23,6 +23,7 @@ export declare class SparkPoDetailComponent {
23
23
  */
24
24
  protected readonly detailPanels: import("@mintplayer/ng-spark/panels").SparkDetailPanel[];
25
25
  protected readonly detailActions: import("@mintplayer/ng-spark/panels").SparkDetailAction[];
26
+ private readonly detailLayouts;
26
27
  showCustomActions: import("@angular/core").InputSignal<boolean>;
27
28
  extraActionsTemplate: import("@angular/core").InputSignal<TemplateRef<void> | null>;
28
29
  extraContentTemplate: import("@angular/core").InputSignal<TemplateRef<{
@@ -86,6 +87,8 @@ export declare class SparkPoDetailComponent {
86
87
  protected readonly isDeletedView: import("@angular/core").Signal<boolean>;
87
88
  /** What every add-on panel and action receives; rebuilt whenever the object or rights change. */
88
89
  protected readonly detailContext: import("@angular/core").Signal<SparkDetailContext | null>;
90
+ /** The registered layout that draws this object's attributes instead of the tabs (#504), if one matches. */
91
+ protected readonly detailLayout: import("@angular/core").Signal<import("@mintplayer/ng-spark/panels").SparkDetailLayout | null>;
89
92
  /**
90
93
  * The actions actually offered for the object on screen.
91
94
  *