@mintplayer/ng-spark 22.26.0 → 22.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"mintplayer-ng-spark-query-list.mjs","names":[],"sources":["../../query-list/src/spark-query-list.component.ts","../../query-list/src/spark-query-list.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, viewChild, TemplateRef } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { Subscription } from 'rxjs';\nimport { CommonModule, NgTemplateOutlet } from '@angular/common';\nimport { ActivatedRoute, ParamMap, Router } from '@angular/router';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsBadgeComponent } from '@mintplayer/ng-bootstrap/badge';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';\nimport { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { SparkService, SparkStreamingService, SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport { TranslateKeyPipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkQueryGridComponent, SparkQueryToolbarAction, SparkSearchBoxComponent, sparkActionClass } from '@mintplayer/ng-spark/grid';\nimport {\n CustomActionDefinition,\n StreamingMessage,\n QueryColumn,\n QueryColumnFilter,\n QueryResultItem,\n SparkDeletedFilter,\n} from '@mintplayer/ng-spark/models';\nimport { NgComponentOutlet } from '@angular/common';\nimport {\n SPARK_QUERY_LIST_ACTIONS,\n SparkQueryListContext,\n orderSparkExtensions,\n parseSparkDeletedParam,\n} from '@mintplayer/ng-spark/panels';\n\n/**\n * The routed query page: chrome around one {@link SparkQueryGridComponent}.\n *\n * It owns what is genuinely page-shaped and route-shaped, and nothing else:\n *\n * - **Route resolution.** It has no `queryId` input; it reads `paramMap`, and it serves two\n * routes — `query/:queryId`, and `po/:type`, which resolves an entity type to a query. That\n * second one is type-to-query resolution, not query rendering, and is why this component still\n * exists rather than the router pointing at the grid.\n * - **Streaming.** The websocket lives here so it stays out of every PO detail page's bundle;\n * the snapshot is filtered and sorted client-side and handed to the grid as `[data]`.\n * - The action bar, the caption, the LIVE badge, the search box and the New button.\n *\n * The grid itself — columns, cells, paging, the row link, selection, custom-action execution —\n * is the shared component. This page previously wrote out `<bs-datatable>` twice, once per\n * transport, with a shared row template between them; both are gone.\n */\n@Component({\n selector: 'spark-query-list',\n imports: [BsBadgeComponent, CommonModule, NgTemplateOutlet, NgComponentOutlet, BsAlertComponent, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsPriorityNavComponent, BsPriorityNavItemDirective, BsSpinnerComponent, SparkIconComponent, SparkQueryGridComponent, SparkSearchBoxComponent, ResolveTranslationPipe, TranslateKeyPipe],\n templateUrl: './spark-query-list.component.html',\n styleUrl: './spark-query-list.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class.virtual-scrolling]': 'isVirtualScrolling()'\n }\n})\nexport class SparkQueryListComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n private readonly streamingService = inject(SparkStreamingService);\n private readonly destroyRef = inject(DestroyRef);\n protected readonly lang = inject(SparkLanguageService);\n\n extraActionsTemplate = input<TemplateRef<void> | null>(null);\n showCustomActions = input(true);\n\n /**\n * Forwarded to the grid, so a query PAGE can replace its row links — the same reason the card\n * forwards it. Without this the escape hatch existed only for a directly-embedded grid.\n */\n rowRoute = input<((row: QueryResultItem) => unknown[] | null) | null>(null);\n\n rowClicked = output<QueryResultItem>();\n createClicked = output<void>();\n customActionExecuted = output<{ action: CustomActionDefinition }>();\n\n colors = Color;\n\n /** The query the grid should render, resolved from the route. Null until it is known. */\n queryId = signal<string | null>(null);\n errorMessage = signal<string | null>(null);\n searchTerm = signal('');\n\n private readonly grid = viewChild(SparkQueryGridComponent);\n\n /**\n * Grid state, surfaced for this page's chrome.\n *\n * Optional `viewChild`, read defensively: the action bar and caption render above the grid, so\n * on the first change-detection pass the query has not resolved yet.\n */\n protected readonly query = computed(() => this.grid()?.query() ?? null);\n protected readonly entityType = computed(() => this.grid()?.entityType() ?? null);\n protected readonly customActions = computed(() => this.grid()?.customActions() ?? []);\n protected readonly canCreate = computed(() => this.grid()?.offersCreate() ?? false);\n protected readonly resultCount = computed(() => this.grid()?.resultCount() ?? null);\n protected readonly isVirtualScrolling = computed(() => this.grid()?.isVirtualScrolling() ?? false);\n protected readonly gridError = computed(() => this.grid()?.errorMessage() ?? null);\n protected readonly permissions = computed(() => this.grid()?.permissions() ?? null);\n\n /** Add-on buttons for the action bar (#460, `SPARK_QUERY_LIST_ACTIONS`), e.g. the Deleted toggle. */\n protected readonly listActions = orderSparkExtensions(inject(SPARK_QUERY_LIST_ACTIONS, { optional: true }), a => a.priority ?? 60);\n\n /**\n * The soft-deletion mode (#460, T2), read from the route's `?deleted=` so the recycle bin survives\n * a reload and the back button from a row opened in it. `exclude` unless the route says otherwise.\n */\n protected readonly deletedMode = signal<SparkDeletedFilter>('exclude');\n\n /** Bumped by `context.reload()`; the grid treats any new value as \"re-run the query\". */\n private readonly reloadToken = signal(0);\n\n protected readonly listContext = computed((): SparkQueryListContext | null => {\n const query = this.query();\n if (!query) return null;\n return {\n query,\n entityType: this.entityType(),\n permissions: this.permissions(),\n deleted: this.deletedMode(),\n setDeleted: (mode: SparkDeletedFilter) => this.setDeleted(mode),\n reload: () => this.reloadToken.update(n => n + 1),\n };\n });\n\n protected readonly gridReloadToken = this.reloadToken.asReadonly();\n\n private setDeleted(mode: SparkDeletedFilter): void {\n this.router.navigate([], {\n relativeTo: this.route,\n queryParams: { deleted: mode === 'exclude' ? null : mode },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n });\n }\n\n /** Whether an action's selection rule is satisfied. Delegated: the grid holds the selection. */\n /** @see SparkPoDetailComponent.customActionClass — same allow-list, same default. */\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 /** The grid's toolbar model (#460, M15), the same list the sub-query card renders. */\n protected readonly toolbarActions = computed(() => this.grid()?.toolbarActions() ?? []);\n\n protected toolbarActionClass(action: SparkQueryToolbarAction): string {\n // New keeps its solid primary look; the rest follow their variant.\n return action.kind === 'new' ? 'btn btn-primary' : sparkActionClass(action.definition);\n }\n\n protected isToolbarActionEnabled(action: SparkQueryToolbarAction): boolean {\n return this.grid()?.isToolbarActionEnabled(action) ?? false;\n }\n\n protected runToolbarAction(action: SparkQueryToolbarAction): void {\n if (action.kind === 'new') {\n // Through this page's own onCreate, so `createClicked` fires exactly as it always has.\n this.onCreate();\n return;\n }\n void this.grid()?.runToolbarAction(action);\n }\n\n protected isActionEnabled(action: CustomActionDefinition): boolean {\n return this.grid()?.isActionEnabled(action) ?? false;\n }\n\n // --- streaming ---------------------------------------------------------------\n\n isStreaming = signal(false);\n private streamingSub: Subscription | null = null;\n /** Columns as sent with the stream's snapshot; empty until it arrives. */\n protected readonly streamColumns = signal<QueryColumn[]>([]);\n private readonly allItems = signal<QueryResultItem[]>([]);\n private readonly streamItems = signal<QueryResultItem[]>([]);\n\n /**\n * Rows handed to the grid, or `null` to let it fetch for itself.\n *\n * Null for a normal query — an empty array would read as \"here are no rows\" and suppress the\n * fetch entirely.\n */\n protected readonly gridData = computed(() =>\n this.query()?.isStreamingQuery ? this.streamItems() : null);\n\n constructor() {\n this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => {\n // The handler is async and this is a subscribe, so a rejection lands nowhere: the metadata\n // load would reject, the query would stay null, and the template would render a spinner\n // FOREVER — while this component has had an errorMessage surface all along that only the\n // fetch path ever reached.\n this.onParamsChange(params).catch((e: unknown) => this.reportLoadFailure(e as HttpErrorResponse));\n });\n\n // Separate from paramMap: switching the recycle bin on changes only the query string, and must\n // not re-resolve the query (which would reset page, sort and filters for nothing).\n this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe(query => {\n this.deletedMode.set(parseSparkDeletedParam(query.get('deleted')) ?? 'exclude');\n this.applyUrlScope(query);\n });\n\n this.destroyRef.onDestroy(() => this.disconnectStreaming());\n\n // Connect the socket once the grid has resolved a streaming query, and disconnect whenever it\n // resolves anything else. The grid knows not to fetch for a streaming query, so there is no\n // window in which both transports are live.\n effect(() => {\n const q = this.query();\n if (q?.isStreamingQuery) {\n this.connectStreaming(q.id);\n } else {\n this.disconnectStreaming();\n }\n });\n\n // Client-side filter and sort for the streaming snapshot. The server never sees these: there\n // is no request to attach them to.\n effect(() => {\n this.searchTerm();\n this.grid()?.settings();\n this.allItems();\n if (this.isStreaming()) this.applyFilter();\n });\n }\n\n // --- URL scope: parent and column filters ----------------------------------------------------\n\n /**\n * The parent the URL scopes the query to (`?parentId=…&parentType=…`), sent exactly as a sub-query\n * sends its container. Both or neither: one without the other is ignored, as the server does.\n */\n protected readonly urlParentId = signal('');\n protected readonly urlParentType = signal('');\n\n /**\n * Column filters from the URL: every other query-string parameter, `?Language=en&Script=Latn`\n * (repeat a name for several values). The grid applies only those naming an attribute of the\n * query's type; the rest are ignored. This is the contributions History link's shape, and works for\n * any query.\n */\n protected readonly urlFilters = signal<QueryColumnFilter[]>([]);\n\n /** The URL filters the grid applied, as removable chips. */\n protected readonly activeUrlFilters = computed(() => this.grid()?.appliedPresetFilters() ?? []);\n\n /** Query-string names the page or the row links own, never column filters. */\n private static readonly reservedParams = new Set(['deleted', 'parentId', 'parentType', 'queryId', 'parentDeleted']);\n\n private applyUrlScope(query: ParamMap): void {\n const parentId = query.get('parentId') ?? '';\n const parentType = query.get('parentType') ?? '';\n const both = !!parentId && !!parentType;\n if (this.urlParentId() !== (both ? parentId : '')) this.urlParentId.set(both ? parentId : '');\n if (this.urlParentType() !== (both ? parentType : '')) this.urlParentType.set(both ? parentType : '');\n\n const filters: QueryColumnFilter[] = query.keys\n .filter(key => !SparkQueryListComponent.reservedParams.has(key))\n .map(key => ({ name: key, includes: query.getAll(key).filter(v => v !== '') }))\n .filter(f => f.includes.length > 0);\n // Only on a real change: a new array would refetch the grid for nothing (the Deleted toggle\n // also changes the query string).\n if (JSON.stringify(filters) !== JSON.stringify(this.urlFilters())) this.urlFilters.set(filters);\n }\n\n /** Drops one URL filter (its query-string parameter); the grid refetches. */\n protected clearUrlFilter(name: string): void {\n this.router.navigate([], { relativeTo: this.route, queryParams: { [name]: null }, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n /** Drops the parent and every URL filter. */\n protected clearUrlScope(): void {\n const queryParams: Record<string, null> = { parentId: null, parentType: null };\n for (const filter of this.urlFilters()) queryParams[filter.name] = null;\n this.router.navigate([], { relativeTo: this.route, queryParams, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n protected filterValues(filter: QueryColumnFilter): string {\n return (filter.includes ?? []).map(v => String(v)).join(', ');\n }\n\n /** The column's label for a chip: the query's own column label, else the attribute's, else its name. */\n protected filterLabel(name: string): string {\n const attribute = this.entityType()?.attributes?.find(a => a.name === name);\n return this.lang.resolve(attribute?.label) || name;\n }\n\n private async onParamsChange(params: ParamMap): Promise<void> {\n this.errorMessage.set(null);\n this.queryId.set(null);\n this.allItems.set([]);\n this.streamItems.set([]);\n this.streamColumns.set([]);\n this.disconnectStreaming();\n\n const queryId = params.get('queryId');\n const typeParam = params.get('type');\n\n if (queryId) {\n this.queryId.set(queryId);\n return;\n }\n\n if (!typeParam) return;\n\n // `po/:type` — the route names an entity type, so find the query that lists it. The grid takes\n // a query, and this translation is the only reason it cannot take the route directly.\n const [entityTypes, queries] = await Promise.all([\n this.sparkService.getEntityTypes(),\n this.sparkService.getQueries(),\n ]);\n const entityType = entityTypes.find(t => t.id === typeParam || t.alias === typeParam);\n if (!entityType) {\n this.reportLoadFailure({ status: 404 } as HttpErrorResponse);\n return;\n }\n\n const singularName = entityType.name;\n const match = queries.find(q => {\n if (q.entityType === singularName) return true;\n const sourceName = q.source.includes('.') ? q.source.substring(q.source.indexOf('.') + 1) : q.source;\n return sourceName === singularName || sourceName === singularName + 's';\n });\n\n if (match) this.queryId.set(match.alias || match.id);\n else this.reportLoadFailure({ status: 404 } as HttpErrorResponse);\n }\n\n /**\n * A load failure has to render, not just be swallowed: a denied query answers 404 (audit M-3, so\n * existence is not leaked), which is indistinguishable from a missing one — hence a deliberately\n * generic message rather than a guess at which it was.\n */\n private reportLoadFailure(err: HttpErrorResponse): void {\n this.errorMessage.set(\n err?.status === 404\n ? (this.lang.t('spark.query.unavailable') || 'This list is not available.')\n : (err?.error?.error || err?.message || 'An unexpected error occurred'));\n }\n\n protected async onCustomAction(action: CustomActionDefinition): Promise<void> {\n await this.grid()?.onCustomAction(action);\n }\n\n protected onCreate(): void {\n this.createClicked.emit();\n const et = this.entityType();\n if (et) this.router.navigate(['/po', et.alias || et.id, 'new']);\n }\n\n protected clearSearch(): void {\n this.searchTerm.set('');\n }\n\n private connectStreaming(queryId: string): void {\n if (this.streamingSub) return;\n this.isStreaming.set(true);\n\n this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({\n next: (message) => this.handleStreamingMessage(message),\n error: (err) => {\n this.errorMessage.set(err?.message || 'Streaming connection failed');\n this.isStreaming.set(false);\n },\n complete: () => this.isStreaming.set(false),\n });\n }\n\n private disconnectStreaming(): void {\n if (this.streamingSub) {\n this.streamingSub.unsubscribe();\n this.streamingSub = null;\n }\n this.isStreaming.set(false);\n }\n\n private handleStreamingMessage(message: StreamingMessage): void {\n switch (message.type) {\n case 'snapshot':\n this.errorMessage.set(null);\n // Columns come with the snapshot and never change for the life of the stream, so they are\n // stored once and handed to the grid alongside the rows — a projection cannot describe\n // itself, and the grid has no entity-type metadata to fall back on any more.\n this.streamColumns.set(message.columns);\n this.allItems.set(message.data);\n break;\n\n case 'patch':\n if (message.updated.length > 0) {\n this.allItems.update(items => items.map(item => {\n const patch = message.updated.find(u => u.id === item.id);\n if (!patch) return item;\n return {\n ...item,\n values: item.values.map(v =>\n v.key in patch.values ? { ...v, value: patch.values[v.key] } : v),\n };\n }));\n }\n break;\n\n case 'error':\n this.errorMessage.set(message.message);\n break;\n }\n }\n\n private applyFilter(): void {\n let items = this.allItems();\n\n const term = this.searchTerm().toLowerCase();\n if (term) {\n items = items.filter(item =>\n item.values.some(v => String(v.value ?? '').toLowerCase().includes(term)));\n }\n\n // The datatable in `[data]` mode also sorts on header clicks, but the sort must survive a\n // patch: re-deriving from `allItems` without re-applying it would silently reorder the grid\n // under the user on every update.\n const sortCols = this.grid()?.settings().sortColumns ?? [];\n if (sortCols.length > 0) {\n items = [...items].sort((a, b) => {\n for (const col of sortCols) {\n const aVal = a.values.find(v => v.key === col.property)?.value ?? '';\n const bVal = b.values.find(v => v.key === col.property)?.value ?? '';\n const cmp = String(aVal).localeCompare(String(bVal));\n if (cmp !== 0) return col.direction === 'descending' ? -cmp : cmp;\n }\n return 0;\n });\n }\n\n this.streamItems.set(items);\n }\n}\n","<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\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_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; 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\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n"],"mappings":"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"}
1
+ {"version":3,"file":"mintplayer-ng-spark-query-list.mjs","names":[],"sources":["../../query-list/src/spark-query-list.component.ts","../../query-list/src/spark-query-list.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, viewChild, TemplateRef } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { Subscription } from 'rxjs';\nimport { CommonModule, NgTemplateOutlet } from '@angular/common';\nimport { ActivatedRoute, ParamMap, Router } from '@angular/router';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsBadgeComponent } from '@mintplayer/ng-bootstrap/badge';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';\nimport { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { SparkService, SparkStreamingService, SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport { TranslateKeyPipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkQueryGridComponent, SparkQueryToolbarAction, SparkSearchBoxComponent, sparkActionClass } from '@mintplayer/ng-spark/grid';\nimport {\n CustomActionDefinition,\n StreamingMessage,\n QueryColumn,\n QueryColumnFilter,\n QueryResultItem,\n SparkDeletedFilter,\n type SparkSelectionModeSetting,\n} from '@mintplayer/ng-spark/models';\nimport { NgComponentOutlet } from '@angular/common';\nimport {\n SPARK_QUERY_LIST_ACTIONS,\n SparkQueryListContext,\n orderSparkExtensions,\n parseSparkDeletedParam,\n} from '@mintplayer/ng-spark/panels';\n\n/**\n * The routed query page: chrome around one {@link SparkQueryGridComponent}.\n *\n * It owns what is genuinely page-shaped and route-shaped, and nothing else:\n *\n * - **Route resolution.** It has no `queryId` input; it reads `paramMap`, and it serves two\n * routes — `query/:queryId`, and `po/:type`, which resolves an entity type to a query. That\n * second one is type-to-query resolution, not query rendering, and is why this component still\n * exists rather than the router pointing at the grid.\n * - **Streaming.** The websocket lives here so it stays out of every PO detail page's bundle;\n * the snapshot is filtered and sorted client-side and handed to the grid as `[data]`.\n * - The action bar, the caption, the LIVE badge, the search box and the New button.\n *\n * The grid itself — columns, cells, paging, the row link, selection, custom-action execution —\n * is the shared component. This page previously wrote out `<bs-datatable>` twice, once per\n * transport, with a shared row template between them; both are gone.\n */\n@Component({\n selector: 'spark-query-list',\n imports: [BsBadgeComponent, CommonModule, NgTemplateOutlet, NgComponentOutlet, BsAlertComponent, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsPriorityNavComponent, BsPriorityNavItemDirective, BsSpinnerComponent, SparkIconComponent, SparkQueryGridComponent, SparkSearchBoxComponent, ResolveTranslationPipe, TranslateKeyPipe],\n templateUrl: './spark-query-list.component.html',\n styleUrl: './spark-query-list.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class.virtual-scrolling]': 'isVirtualScrolling()'\n }\n})\nexport class SparkQueryListComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n private readonly streamingService = inject(SparkStreamingService);\n private readonly destroyRef = inject(DestroyRef);\n protected readonly lang = inject(SparkLanguageService);\n\n extraActionsTemplate = input<TemplateRef<void> | null>(null);\n showCustomActions = input(true);\n\n /**\n * Overrides the query's own `selectionMode` (#467, R3), as the sub-query card's input does: for a\n * host that embeds this page and wants, say, `'none'` on a list the actions would make selectable.\n */\n selectionMode = input<SparkSelectionModeSetting | null>(null);\n\n /**\n * Forwarded to the grid, so a query PAGE can replace its row links — the same reason the card\n * forwards it. Without this the escape hatch existed only for a directly-embedded grid.\n */\n rowRoute = input<((row: QueryResultItem) => unknown[] | null) | null>(null);\n\n rowClicked = output<QueryResultItem>();\n createClicked = output<void>();\n customActionExecuted = output<{ action: CustomActionDefinition }>();\n\n colors = Color;\n\n /** The query the grid should render, resolved from the route. Null until it is known. */\n queryId = signal<string | null>(null);\n errorMessage = signal<string | null>(null);\n searchTerm = signal('');\n\n private readonly grid = viewChild(SparkQueryGridComponent);\n\n /**\n * Grid state, surfaced for this page's chrome.\n *\n * Optional `viewChild`, read defensively: the action bar and caption render above the grid, so\n * on the first change-detection pass the query has not resolved yet.\n */\n protected readonly query = computed(() => this.grid()?.query() ?? null);\n protected readonly entityType = computed(() => this.grid()?.entityType() ?? null);\n protected readonly customActions = computed(() => this.grid()?.customActions() ?? []);\n protected readonly canCreate = computed(() => this.grid()?.offersCreate() ?? false);\n protected readonly resultCount = computed(() => this.grid()?.resultCount() ?? null);\n protected readonly isVirtualScrolling = computed(() => this.grid()?.isVirtualScrolling() ?? false);\n protected readonly gridError = computed(() => this.grid()?.errorMessage() ?? null);\n protected readonly permissions = computed(() => this.grid()?.permissions() ?? null);\n\n /** Add-on buttons for the action bar (#460, `SPARK_QUERY_LIST_ACTIONS`), e.g. the Deleted toggle. */\n protected readonly listActions = orderSparkExtensions(inject(SPARK_QUERY_LIST_ACTIONS, { optional: true }), a => a.priority ?? 60);\n\n /**\n * The soft-deletion mode (#460, T2), read from the route's `?deleted=` so the recycle bin survives\n * a reload and the back button from a row opened in it. `exclude` unless the route says otherwise.\n */\n protected readonly deletedMode = signal<SparkDeletedFilter>('exclude');\n\n /** Bumped by `context.reload()`; the grid treats any new value as \"re-run the query\". */\n private readonly reloadToken = signal(0);\n\n protected readonly listContext = computed((): SparkQueryListContext | null => {\n const query = this.query();\n if (!query) return null;\n return {\n query,\n entityType: this.entityType(),\n permissions: this.permissions(),\n deleted: this.deletedMode(),\n setDeleted: (mode: SparkDeletedFilter) => this.setDeleted(mode),\n reload: () => this.reloadToken.update(n => n + 1),\n };\n });\n\n protected readonly gridReloadToken = this.reloadToken.asReadonly();\n\n private setDeleted(mode: SparkDeletedFilter): void {\n this.router.navigate([], {\n relativeTo: this.route,\n queryParams: { deleted: mode === 'exclude' ? null : mode },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n });\n }\n\n /** Whether an action's selection rule is satisfied. Delegated: the grid holds the selection. */\n /** @see SparkPoDetailComponent.customActionClass — same allow-list, same default. */\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 /** The grid's toolbar model (#460, M15), the same list the sub-query card renders. */\n protected readonly toolbarActions = computed(() => this.grid()?.toolbarActions() ?? []);\n\n protected toolbarActionClass(action: SparkQueryToolbarAction): string {\n // New keeps its solid primary look; the rest follow their variant.\n return action.kind === 'new' ? 'btn btn-primary' : sparkActionClass(action.definition);\n }\n\n protected isToolbarActionEnabled(action: SparkQueryToolbarAction): boolean {\n return this.grid()?.isToolbarActionEnabled(action) ?? false;\n }\n\n protected runToolbarAction(action: SparkQueryToolbarAction): void {\n if (action.kind === 'new') {\n // Through this page's own onCreate, so `createClicked` fires exactly as it always has.\n this.onCreate();\n return;\n }\n void this.grid()?.runToolbarAction(action);\n }\n\n protected isActionEnabled(action: CustomActionDefinition): boolean {\n return this.grid()?.isActionEnabled(action) ?? false;\n }\n\n // --- streaming ---------------------------------------------------------------\n\n isStreaming = signal(false);\n private streamingSub: Subscription | null = null;\n /** Columns as sent with the stream's snapshot; empty until it arrives. */\n protected readonly streamColumns = signal<QueryColumn[]>([]);\n private readonly allItems = signal<QueryResultItem[]>([]);\n private readonly streamItems = signal<QueryResultItem[]>([]);\n\n /**\n * Rows handed to the grid, or `null` to let it fetch for itself.\n *\n * Null for a normal query — an empty array would read as \"here are no rows\" and suppress the\n * fetch entirely.\n */\n protected readonly gridData = computed(() =>\n this.query()?.isStreamingQuery ? this.streamItems() : null);\n\n constructor() {\n this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => {\n // The handler is async and this is a subscribe, so a rejection lands nowhere: the metadata\n // load would reject, the query would stay null, and the template would render a spinner\n // FOREVER — while this component has had an errorMessage surface all along that only the\n // fetch path ever reached.\n this.onParamsChange(params).catch((e: unknown) => this.reportLoadFailure(e as HttpErrorResponse));\n });\n\n // Separate from paramMap: switching the recycle bin on changes only the query string, and must\n // not re-resolve the query (which would reset page, sort and filters for nothing).\n this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe(query => {\n this.deletedMode.set(parseSparkDeletedParam(query.get('deleted')) ?? 'exclude');\n this.applyUrlScope(query);\n });\n\n this.destroyRef.onDestroy(() => this.disconnectStreaming());\n\n // Connect the socket once the grid has resolved a streaming query, and disconnect whenever it\n // resolves anything else. The grid knows not to fetch for a streaming query, so there is no\n // window in which both transports are live.\n effect(() => {\n const q = this.query();\n if (q?.isStreamingQuery) {\n this.connectStreaming(q.id);\n } else {\n this.disconnectStreaming();\n }\n });\n\n // Client-side filter and sort for the streaming snapshot. The server never sees these: there\n // is no request to attach them to.\n effect(() => {\n this.searchTerm();\n this.grid()?.settings();\n this.allItems();\n if (this.isStreaming()) this.applyFilter();\n });\n }\n\n // --- URL scope: parent and column filters ----------------------------------------------------\n\n /**\n * The parent the URL scopes the query to (`?parentId=…&parentType=…`), sent exactly as a sub-query\n * sends its container. Both or neither: one without the other is ignored, as the server does.\n */\n protected readonly urlParentId = signal('');\n protected readonly urlParentType = signal('');\n\n /**\n * Column filters from the URL: every other query-string parameter, `?Language=en&Script=Latn`\n * (repeat a name for several values). The grid applies only those naming an attribute of the\n * query's type; the rest are ignored. This is the contributions History link's shape, and works for\n * any query.\n */\n protected readonly urlFilters = signal<QueryColumnFilter[]>([]);\n\n /** The URL filters the grid applied, as removable chips. */\n protected readonly activeUrlFilters = computed(() => this.grid()?.appliedPresetFilters() ?? []);\n\n /** Query-string names the page or the row links own, never column filters. */\n private static readonly reservedParams = new Set(['deleted', 'parentId', 'parentType', 'queryId', 'parentDeleted']);\n\n private applyUrlScope(query: ParamMap): void {\n const parentId = query.get('parentId') ?? '';\n const parentType = query.get('parentType') ?? '';\n const both = !!parentId && !!parentType;\n if (this.urlParentId() !== (both ? parentId : '')) this.urlParentId.set(both ? parentId : '');\n if (this.urlParentType() !== (both ? parentType : '')) this.urlParentType.set(both ? parentType : '');\n\n const filters: QueryColumnFilter[] = query.keys\n .filter(key => !SparkQueryListComponent.reservedParams.has(key))\n .map(key => ({ name: key, includes: query.getAll(key).filter(v => v !== '') }))\n .filter(f => f.includes.length > 0);\n // Only on a real change: a new array would refetch the grid for nothing (the Deleted toggle\n // also changes the query string).\n if (JSON.stringify(filters) !== JSON.stringify(this.urlFilters())) this.urlFilters.set(filters);\n }\n\n /** Drops one URL filter (its query-string parameter); the grid refetches. */\n protected clearUrlFilter(name: string): void {\n this.router.navigate([], { relativeTo: this.route, queryParams: { [name]: null }, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n /** Drops the parent and every URL filter. */\n protected clearUrlScope(): void {\n const queryParams: Record<string, null> = { parentId: null, parentType: null };\n for (const filter of this.urlFilters()) queryParams[filter.name] = null;\n this.router.navigate([], { relativeTo: this.route, queryParams, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n protected filterValues(filter: QueryColumnFilter): string {\n return (filter.includes ?? []).map(v => String(v)).join(', ');\n }\n\n /** The column's label for a chip: the query's own column label, else the attribute's, else its name. */\n protected filterLabel(name: string): string {\n const attribute = this.entityType()?.attributes?.find(a => a.name === name);\n return this.lang.resolve(attribute?.label) || name;\n }\n\n private async onParamsChange(params: ParamMap): Promise<void> {\n this.errorMessage.set(null);\n this.queryId.set(null);\n this.allItems.set([]);\n this.streamItems.set([]);\n this.streamColumns.set([]);\n this.disconnectStreaming();\n\n const queryId = params.get('queryId');\n const typeParam = params.get('type');\n\n if (queryId) {\n this.queryId.set(queryId);\n return;\n }\n\n if (!typeParam) return;\n\n // `po/:type` — the route names an entity type, so find the query that lists it. The grid takes\n // a query, and this translation is the only reason it cannot take the route directly.\n const [entityTypes, queries] = await Promise.all([\n this.sparkService.getEntityTypes(),\n this.sparkService.getQueries(),\n ]);\n const entityType = entityTypes.find(t => t.id === typeParam || t.alias === typeParam);\n if (!entityType) {\n this.reportLoadFailure({ status: 404 } as HttpErrorResponse);\n return;\n }\n\n const singularName = entityType.name;\n const match = queries.find(q => {\n if (q.entityType === singularName) return true;\n const sourceName = q.source.includes('.') ? q.source.substring(q.source.indexOf('.') + 1) : q.source;\n return sourceName === singularName || sourceName === singularName + 's';\n });\n\n if (match) this.queryId.set(match.alias || match.id);\n else this.reportLoadFailure({ status: 404 } as HttpErrorResponse);\n }\n\n /**\n * A load failure has to render, not just be swallowed: a denied query answers 404 (audit M-3, so\n * existence is not leaked), which is indistinguishable from a missing one — hence a deliberately\n * generic message rather than a guess at which it was.\n */\n private reportLoadFailure(err: HttpErrorResponse): void {\n this.errorMessage.set(\n err?.status === 404\n ? (this.lang.t('spark.query.unavailable') || 'This list is not available.')\n : (err?.error?.error || err?.message || 'An unexpected error occurred'));\n }\n\n protected async onCustomAction(action: CustomActionDefinition): Promise<void> {\n await this.grid()?.onCustomAction(action);\n }\n\n protected onCreate(): void {\n this.createClicked.emit();\n const et = this.entityType();\n if (et) this.router.navigate(['/po', et.alias || et.id, 'new']);\n }\n\n protected clearSearch(): void {\n this.searchTerm.set('');\n }\n\n private connectStreaming(queryId: string): void {\n if (this.streamingSub) return;\n this.isStreaming.set(true);\n\n this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({\n next: (message) => this.handleStreamingMessage(message),\n error: (err) => {\n this.errorMessage.set(err?.message || 'Streaming connection failed');\n this.isStreaming.set(false);\n },\n complete: () => this.isStreaming.set(false),\n });\n }\n\n private disconnectStreaming(): void {\n if (this.streamingSub) {\n this.streamingSub.unsubscribe();\n this.streamingSub = null;\n }\n this.isStreaming.set(false);\n }\n\n private handleStreamingMessage(message: StreamingMessage): void {\n switch (message.type) {\n case 'snapshot':\n this.errorMessage.set(null);\n // Columns come with the snapshot and never change for the life of the stream, so they are\n // stored once and handed to the grid alongside the rows — a projection cannot describe\n // itself, and the grid has no entity-type metadata to fall back on any more.\n this.streamColumns.set(message.columns);\n this.allItems.set(message.data);\n break;\n\n case 'patch':\n if (message.updated.length > 0) {\n this.allItems.update(items => items.map(item => {\n const patch = message.updated.find(u => u.id === item.id);\n if (!patch) return item;\n return {\n ...item,\n values: item.values.map(v =>\n v.key in patch.values ? { ...v, value: patch.values[v.key] } : v),\n };\n }));\n }\n break;\n\n case 'error':\n this.errorMessage.set(message.message);\n break;\n }\n }\n\n private applyFilter(): void {\n let items = this.allItems();\n\n const term = this.searchTerm().toLowerCase();\n if (term) {\n items = items.filter(item =>\n item.values.some(v => String(v.value ?? '').toLowerCase().includes(term)));\n }\n\n // The datatable in `[data]` mode also sorts on header clicks, but the sort must survive a\n // patch: re-deriving from `allItems` without re-applying it would silently reorder the grid\n // under the user on every update.\n const sortCols = this.grid()?.settings().sortColumns ?? [];\n if (sortCols.length > 0) {\n items = [...items].sort((a, b) => {\n for (const col of sortCols) {\n const aVal = a.values.find(v => v.key === col.property)?.value ?? '';\n const bVal = b.values.find(v => v.key === col.property)?.value ?? '';\n const cmp = String(aVal).localeCompare(String(bVal));\n if (cmp !== 0) return col.direction === 'descending' ? -cmp : cmp;\n }\n return 0;\n });\n }\n\n this.streamItems.set(items);\n }\n}\n","<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460 M15, #467 R3): New, Edit, Delete and the custom actions —\n the list the sub-query card renders too, labels and icons from the action catalogue.\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.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_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; 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\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.label | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n"],"mappings":"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"}
@@ -273,13 +273,14 @@ var SparkService = class SparkService {
273
273
  });
274
274
  }
275
275
  async create(type, data, parent) {
276
- return this.postWithEnvelope(`${this.baseUrl}/po/create`, {
276
+ return await this.postWithEnvelope(`${this.baseUrl}/po/create`, {
277
277
  objectTypeId: type,
278
278
  persistentObject: data,
279
279
  ...parent ?? {}
280
- });
280
+ }) ?? null;
281
281
  }
282
282
  async update(type, id, data) {
283
+ if (!data.etag) throw new Error(`An update of '${id}' must carry the etag it was loaded with.`);
283
284
  return this.postWithEnvelope(`${this.baseUrl}/po/update`, {
284
285
  objectTypeId: type,
285
286
  id,
@@ -305,17 +306,18 @@ var SparkService = class SparkService {
305
306
  ...options
306
307
  });
307
308
  }
308
- async delete(type, id) {
309
+ async delete(type, id, etag) {
309
310
  return this.postWithEnvelope(`${this.baseUrl}/po/delete`, {
310
311
  objectTypeId: type,
311
- id
312
+ id,
313
+ etag
312
314
  });
313
315
  }
314
- async deleteMany(type, ids, options) {
316
+ async deleteMany(type, items, options) {
315
317
  return this.postWithEnvelope(`${this.baseUrl}/po/delete-many`, {
316
318
  objectTypeId: type,
317
- ids,
318
- ...options ?? {}
319
+ items,
320
+ ...options
319
321
  });
320
322
  }
321
323
  async getCustomActions(objectTypeId) {
@@ -1 +1 @@
1
- {"version":3,"file":"mintplayer-ng-spark-services.mjs","names":[],"sources":["../../services/src/retry-action.service.ts","../../services/src/spark-language.service.ts","../../services/src/spark-streaming.service.ts","../../services/src/spark.service.ts","../../services/src/spark-built-in-icons.ts","../../services/src/spark-icon-registry.ts","../../services/src/spark-query-actions.service.ts","../../services/src/spark-timezone.interceptor.ts","../../services/src/spark-return-navigation.service.ts"],"sourcesContent":["import { Injectable, signal } from '@angular/core';\nimport { RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\n\n@Injectable({ providedIn: 'root' })\nexport class RetryActionService {\n private resolveRetry: ((result: RetryActionResult) => void) | null = null;\n\n payload = signal<RetryActionPayload | null>(null);\n\n show(payload: RetryActionPayload): Promise<RetryActionResult> {\n this.payload.set(payload);\n return new Promise(resolve => { this.resolveRetry = resolve; });\n }\n\n respond(result: RetryActionResult): void {\n this.payload.set(null);\n this.resolveRetry?.(result);\n this.resolveRetry = null;\n }\n}\n","import { Injectable, inject, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { TranslatedString, currentLanguage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\ninterface CultureConfiguration {\n languages: Record<string, TranslatedString>;\n defaultLanguage: string;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class SparkLanguageService {\n private readonly http = inject(HttpClient);\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly currentLang = signal('en');\n private readonly translationsMap = signal<Record<string, TranslatedString>>({});\n\n readonly language = this.currentLang.asReadonly();\n readonly languages = signal<Record<string, TranslatedString>>({});\n\n constructor() {\n this.loadCulture();\n this.loadTranslations();\n }\n\n private async loadCulture(): Promise<void> {\n const config = await firstValueFrom(this.http.get<CultureConfiguration>(`${this.baseUrl}/culture`));\n this.languages.set(config.languages);\n const saved = localStorage.getItem('spark-lang');\n const lang = saved ?? config.defaultLanguage;\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n }\n\n private async loadTranslations(): Promise<void> {\n const t = await firstValueFrom(this.http.get<Record<string, TranslatedString>>(`${this.baseUrl}/translations`));\n this.translationsMap.set(t);\n }\n\n setLanguage(lang: string) {\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n localStorage.setItem('spark-lang', lang);\n }\n\n resolve(ts: TranslatedString | undefined): string {\n if (!ts) return '';\n const lang = this.currentLang();\n return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n }\n\n t(key: string): string {\n const ts = this.translationsMap()[key];\n return this.resolve(ts) || key;\n }\n}\n","import { inject, Injectable, NgZone } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { StreamingMessage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n@Injectable({ providedIn: 'root' })\nexport class SparkStreamingService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly ngZone = inject(NgZone);\n\n connectToStreamingQuery(queryId: string): Observable<StreamingMessage> {\n return new Observable<StreamingMessage>(subscriber => {\n let ws: WebSocket | null = null;\n let retryCount = 0;\n let retryTimeout: ReturnType<typeof setTimeout> | null = null;\n let closed = false;\n const maxRetries = 10;\n const maxDelay = 30000;\n\n const connect = () => {\n if (closed) return;\n\n const url = this.buildWebSocketUrl(queryId);\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n retryCount = 0; // Reset on successful connect\n };\n\n ws.onmessage = (event) => {\n try {\n const message: StreamingMessage = JSON.parse(event.data);\n this.ngZone.run(() => subscriber.next(message));\n } catch {\n // Ignore malformed messages\n }\n };\n\n ws.onerror = () => {\n // Error will trigger onclose\n };\n\n ws.onclose = (event) => {\n if (closed) return;\n\n // Don't reconnect on normal closure\n if (event.code === 1000) {\n this.ngZone.run(() => subscriber.complete());\n return;\n }\n\n // Reconnect with exponential backoff\n if (retryCount < maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, retryCount), maxDelay);\n retryCount++;\n retryTimeout = setTimeout(() => connect(), delay);\n } else {\n this.ngZone.run(() => subscriber.error(new Error('WebSocket connection failed after maximum retries')));\n }\n };\n };\n\n connect();\n\n // Teardown: close WebSocket on unsubscribe\n return () => {\n closed = true;\n if (retryTimeout) {\n clearTimeout(retryTimeout);\n }\n if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {\n ws.close(1000, 'Client unsubscribed');\n }\n };\n });\n }\n\n private buildWebSocketUrl(queryId: string): string {\n const encodedId = encodeURIComponent(queryId);\n const path = `${this.baseUrl}/queries/${encodedId}/stream`;\n\n // If baseUrl is absolute (starts with http/https), replace protocol\n if (this.baseUrl.startsWith('http://')) {\n return path.replace(/^http:\\/\\//, 'ws://');\n }\n if (this.baseUrl.startsWith('https://')) {\n return path.replace(/^https:\\/\\//, 'wss://');\n }\n\n // Relative URL — construct from window.location\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${window.location.host}${path}`;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { HttpClient, HttpErrorResponse } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { CustomActionDefinition, DistinctValuesResult, EntityPermissions, EntityType, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, ProgramUnitsConfiguration, QueryColumnFilter, QueryResult, SparkDeletedFilter, SparkQuery, RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\nimport { ClientOperationEnvelope, RetryOperation, SparkClientOperationDispatcher } from '@mintplayer/ng-spark/client-operations';\nimport { SortColumn } from '@mintplayer/pagination';\nimport { RetryActionService } from './retry-action.service';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n/**\n * Context for {@link SparkService.newObject}. Everything is optional: a standalone New sends an\n * empty body, and an AsDetail row on a parent that has never been saved sends no `parentId` —\n * which is the honest answer, not a gap. The server hands the hook a null parent rather than\n * trusting the client's copy of one.\n */\nexport interface NewObjectOptions {\n /** Name of the parent's AsDetail attribute the row is for; absent for a standalone New. */\n asDetailAttribute?: string;\n /** The parent's entity type — required whenever `asDetailAttribute` is set. */\n parentType?: string;\n /** The parent's id, absent while the parent is itself unsaved. */\n parentId?: string;\n /** Free-form arguments, e.g. which variant a New menu chose. */\n parameters?: Record<string, string>;\n /**\n * The sub-query New was started from (#460, D19), with `parentType` and `parentId` and no\n * `asDetailAttribute`. The server loads the parent, checks the parent's type declares this\n * sub-query, and runs `OnNewAsync` with it — whose base fills the reference to the parent.\n */\n queryId?: string;\n}\n\n/** Where a bulk delete was started from; see {@link SparkService.deleteMany}. */\nexport interface DeleteManyOptions {\n /** The query the rows were selected in, for the server's `OnDisableActionsAsync`. */\n queryId?: string;\n /** The sub-query's container, when deleting from a sub-query. */\n parentId?: string;\n parentType?: string;\n}\n\n/** Context for {@link SparkService.deleteRow}. Every field is required — see `DeleteRow.cs`. */\nexport interface DeleteRowOptions {\n asDetailAttribute: string;\n parentType: string;\n parentId: string;\n /** The row's `__sparkRowKey`, as it arrived from the server. */\n rowKey: string;\n parameters?: Record<string, string>;\n}\n\n/**\n * The union of every body the envelope helpers send.\n *\n * Named rather than inlined per helper because the New and delete-row calls carry neither a\n * `persistentObject` nor a `triggeredBy`: an inline type that did not list their fields would have\n * rejected them at the call site, and spreading them through `any` would have hidden a typo in a\n * field name the server matches by exact spelling.\n */\ntype EnvelopeRequestBody = {\n /** The entity type the request is about — the request parameter, not the submitted object's claim. */\n objectTypeId?: string;\n /** The target object's id, for update and delete. */\n id?: string;\n persistentObject?: any;\n triggeredBy?: string;\n retryResults?: RetryActionResult[];\n /** The rows of a bulk delete. */\n ids?: string[];\n} & Partial<NewObjectOptions> & Partial<DeleteManyOptions> & Partial<Omit<DeleteRowOptions, 'asDetailAttribute' | 'parentType' | 'parentId'>>;\n\n@Injectable({ providedIn: 'root' })\nexport class SparkService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly http = inject(HttpClient);\n private readonly retryActionService = inject(RetryActionService);\n\n /**\n * How many prompts one request will answer before giving up. Matches the .NET client's\n * `SparkClient.MaxRetryDepth`, and for the same reason: this is a termination bound, not a\n * capacity one. Far above any real conversation (Fleet's longest is two) and far below anything\n * that could hide a spin.\n */\n private static readonly MAX_RETRY_DEPTH = 16;\n private readonly dispatcher = inject(SparkClientOperationDispatcher);\n\n // Entity Types\n async getEntityTypes(): Promise<EntityType[]> {\n return firstValueFrom(this.http.get<EntityType[]>(`${this.baseUrl}/types`));\n }\n\n async getEntityType(id: string): Promise<EntityType> {\n return firstValueFrom(this.http.get<EntityType>(`${this.baseUrl}/types/${encodeURIComponent(id)}`));\n }\n\n async getEntityTypeByClrType(clrType: string): Promise<EntityType | undefined> {\n const types = await this.getEntityTypes();\n return types.find(t => t.clrType === clrType);\n }\n\n // Permissions\n async getPermissions(entityTypeId: string): Promise<EntityPermissions> {\n return firstValueFrom(this.http.get<EntityPermissions>(`${this.baseUrl}/permissions/${encodeURIComponent(entityTypeId)}`));\n }\n\n /** Resolved at most once; see getQueryByName. */\n #queryCatalogue?: Promise<SparkQuery[]>;\n\n // Queries\n async getQueries(): Promise<SparkQuery[]> {\n return firstValueFrom(this.http.get<SparkQuery[]>(`${this.baseUrl}/queries`));\n }\n\n async getQuery(id: string): Promise<SparkQuery> {\n return firstValueFrom(this.http.post<SparkQuery>(`${this.baseUrl}/queries/get`, { queryId: id }));\n }\n\n /**\n * Resolves a query by name, fetching the catalogue at most once per service instance.\n *\n * Without the cache this costs a full `GET /spark/queries` per call, and a form with several\n * reference attributes resolves them in parallel — so opening one page fetched the whole query\n * catalogue N times over. The catalogue is per-caller and effectively static for a page's\n * lifetime; the service is scoped to the app, so a permission change lands on the next load.\n */\n async getQueryByName(name: string): Promise<SparkQuery | undefined> {\n this.#queryCatalogue ??= this.getQueries();\n const queries = await this.#queryCatalogue;\n return queries.find(q => q.name === name);\n }\n\n async executeQuery(queryId: string, options?: ExecuteQueryOptions): Promise<QueryResult> {\n // A POST with a typed body, not a GET with a query string. `sortColumns` used to be encoded as\n // `prop:asc,other:desc` — an encoding invented because a query string has no arrays. Column\n // filtering (several columns, several selected values each) would need a second such encoding,\n // which is the reason the reads moved to POST at all.\n return this.sendRead<QueryResult>(`${this.baseUrl}/queries/execute`, {\n queryId,\n sortColumns: options?.sortColumns?.length\n ? options.sortColumns.map(c => ({ property: c.property, direction: c.direction === 'descending' ? 'desc' : 'asc' }))\n : undefined,\n parentId: options?.parentId,\n parentType: options?.parentType,\n skip: options?.skip,\n take: options?.take,\n search: options?.search,\n // Per-column value filters (#431). The shape that could not be expressed as a query string,\n // and the reason these reads are a POST at all.\n columns: options?.columns?.length ? options.columns : undefined,\n // Soft-deletion mode (#460, T2). Omitted = exclude.\n deleted: options?.deleted,\n // The parent's own mode (#460): a deleted parent opened from the recycle bin. Omitted = exclude.\n parentDeleted: options?.parentDeleted,\n });\n }\n\n /**\n * The distinct values behind one column's filter panel (#431).\n *\n * `columns` carries the OTHER columns' current filters, so the values returned are the ones still\n * reachable — offering a value that empties the grid the moment it is picked is worse than\n * omitting it.\n *\n * An empty result means either \"nothing matches\" or \"you may not enumerate this column\". The two\n * are deliberately indistinguishable, so there is nothing for a caller to branch on.\n */\n async getDistinctValues(queryId: string, column: string, options?: {\n /** Narrows the listed values themselves (the panel's own box). */\n search?: string;\n /**\n * The grid's search term, what `executeQuery` sends as `search` (#460 M15): the values come only\n * from the rows it matches, so a searched grid's panel lists what the grid shows.\n */\n querySearch?: string;\n columns?: QueryColumnFilter[];\n parentId?: string;\n parentType?: string;\n /** The same soft-deletion mode as the grid the panel belongs to (#460). */\n deleted?: SparkDeletedFilter;\n /** The same parent mode as the grid the panel belongs to (#460). */\n parentDeleted?: SparkDeletedFilter;\n }): Promise<DistinctValuesResult> {\n return this.sendRead<DistinctValuesResult>(`${this.baseUrl}/queries/distinct-values`, {\n queryId,\n column,\n search: options?.search || undefined,\n querySearch: options?.querySearch || undefined,\n columns: options?.columns?.length ? options.columns : undefined,\n deleted: options?.deleted,\n parentId: options?.parentId,\n parentType: options?.parentType,\n parentDeleted: options?.parentDeleted,\n });\n }\n\n /**\n * Runs a query identified by name.\n *\n * It used to forward only the parent, dropping skip/take/search — so every reference picker and\n * AsDetail reference column silently saw the server's default of 50 candidates, with no indication\n * that the list was cut. A picker that omits the option you are looking for, and says nothing, is\n * worse than one that fails.\n */\n async executeQueryByName(queryName: string, options?: ExecuteQueryOptions): Promise<QueryResult> {\n const query = await this.getQueryByName(queryName);\n if (!query) return { columns: [], items: [], totalItems: 0, skip: 0, take: options?.take ?? 50 };\n return this.executeQuery(query.id, options);\n }\n\n // Program Units\n async getProgramUnits(): Promise<ProgramUnitsConfiguration> {\n return firstValueFrom(this.http.get<ProgramUnitsConfiguration>(`${this.baseUrl}/program-units`));\n }\n\n // Persistent Objects\n /**\n * Loads one object. `options.deleted` (#460) lets a `ViewDeleted` holder open a soft-deleted row\n * from the recycle bin (`'include'` or `'only'`); without the right the server ignores it and a\n * deleted row stays a 404. Omitted = exclude.\n */\n async get(type: string, id: string, options?: { deleted?: SparkDeletedFilter }): Promise<PersistentObject> {\n return this.sendRead<PersistentObject>(`${this.baseUrl}/po/load`, { objectTypeId: type, id, deleted: options?.deleted });\n }\n\n /**\n * `parent` is the sub-query the New was started from — the same `parentId`/`parentType`/`queryId`\n * `newObject` was given. The server resolves and authorizes it again and hands it to the save hooks\n * as `PersistentObject.Parent`.\n */\n async create(\n type: string,\n data: Partial<PersistentObject>,\n parent?: { parentId: string; parentType: string; queryId: string },\n ): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/create`,\n { objectTypeId: type, persistentObject: data, ...(parent ?? {}) }\n );\n }\n\n async update(type: string, id: string, data: Partial<PersistentObject>): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/update`,\n { objectTypeId: type, id, persistentObject: data }\n );\n }\n\n /**\n * Asks the server to reshape an in-progress object after `triggeredBy`'s value changed.\n *\n * Writes nothing, but goes through the envelope like every other mutating call: a refresh may\n * legitimately emit notifications, and may open the retry-action prompt.\n *\n * `triggeredBy` is the attribute's name. For a trigger inside an AsDetail row it is the same\n * path form the inline validation errors use — `Jobs[2].ProfessionId`.\n */\n async refresh(type: string, data: Partial<PersistentObject>, triggeredBy: string): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/refresh`,\n { objectTypeId: type, persistentObject: data, triggeredBy }\n );\n }\n\n /**\n * Asks the server to construct a new object of `type`, so `OnNewAsync` can default it.\n *\n * Writes nothing — the object comes back unsaved, and for an AsDetail row the parent still owns\n * the save. Called by the create page for every New (#460, D19: with `parentId`/`parentType`/\n * `queryId` when started from a sub-query, whose base hook fills the parent reference), and for an\n * AsDetail row only when its type's `serverSideRowLifecycle` is on; every other row type keeps\n * building its blank row locally, which is why switching the flag off costs no request.\n *\n * ⚠️ The row comes back **keyed**: the server constructs the CLR instance, so the row-key field\n * initializer runs and `po.id` carries the key. Flattening it with `nestedPoToDict` therefore\n * populates `__sparkRowKey`, and the row is matchable on save from the moment it is added — the\n * whole reason this round-trip is worth a request.\n */\n async newObject(type: string, options?: NewObjectOptions): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/new`,\n { objectTypeId: type, ...(options ?? {}) }\n );\n }\n\n /**\n * Asks whether a stored row may leave its parent's AsDetail collection.\n *\n * Deletes nothing: a resolved promise means the caller may splice the row out of the collection\n * it is editing, and the removal is persisted with the parent. A hook that refuses rejects with a\n * 400 whose `error.error.result.errors` carries the readable reason, exactly like a save.\n *\n * A POST — as everything is now. It used to be worth explaining: the framework's real delete route\n * was a catch-all that would have swallowed a sibling DELETE. The catch-all is gone with the rest\n * of the route variables.\n */\n async deleteRow(type: string, options: DeleteRowOptions): Promise<void> {\n await this.postWithEnvelope<{ removed: boolean }>(\n `${this.baseUrl}/po/delete-row`,\n { objectTypeId: type, ...options }\n );\n }\n\n async delete(type: string, id: string): Promise<void> {\n return this.postWithEnvelope<void>(\n `${this.baseUrl}/po/delete`,\n { objectTypeId: type, id }\n );\n }\n\n /**\n * Deletes several rows of one type in one request — the default Delete action on a selection\n * (`POST /spark/po/delete-many`, #460 D18). All or nothing: a 404 means some row is missing or not\n * yours to delete, a 403 that the server's `OnDisableActionsAsync` withholds Delete on the query or\n * on one of the rows, a 400 that the selection breaks the rule or the 200-row cap.\n */\n async deleteMany(type: string, ids: string[], options?: DeleteManyOptions): Promise<void> {\n return this.postWithEnvelope<void>(\n `${this.baseUrl}/po/delete-many`,\n { objectTypeId: type, ids, ...(options ?? {}) }\n );\n }\n\n // Custom Actions\n async getCustomActions(objectTypeId: string): Promise<CustomActionDefinition[]> {\n return firstValueFrom(this.http.post<CustomActionDefinition[]>(`${this.baseUrl}/actions/list`, { objectTypeId }));\n }\n\n /**\n * @param parent The object of THIS type the action is operating on — the detail-page invocation.\n * @param selectedItemIds Row ids from a query. Ids, never row objects: a row is a projection.\n * @param queryParent When invoked from a sub-query, the object whose detail page it was rendered\n * on — a DIFFERENT type (the cars listed on a company's page are Cars, the page is a Company).\n * Sent as id + type, exactly as the query endpoint names it, and resolved server-side under its\n * own type with its own Read gate.\n * @param queryId The query the selection came from, so the server can re-run it narrowed to those\n * ids and hand the action the rows the grid actually had -- index-computed columns included.\n * @returns What the action handed to `CustomActionArgs.SetResult` (#460, T5) -- the envelope's\n * `result` -- or `undefined` when it set nothing. The value is the action author's to shape: it\n * bypasses the server's redaction. A 403 means the action is disabled for this object, query or\n * selection (the server's `OnDisableActionsAsync` said so), distinct from a 404 for a row the\n * caller cannot see.\n */\n async executeCustomAction<T = unknown>(\n objectTypeId: string,\n actionName: string,\n parent?: PersistentObject,\n selectedItemIds?: string[],\n queryParent?: { id: string; type: string },\n queryId?: string,\n ): Promise<T | undefined> {\n const body: {\n objectTypeId: string; actionName: string;\n parent?: PersistentObject; selectedItemIds?: string[];\n parentId?: string; parentType?: string; queryId?: string;\n retryResults?: RetryActionResult[];\n } = { objectTypeId, actionName, parent, selectedItemIds, parentId: queryParent?.id, parentType: queryParent?.type, queryId };\n // A literal `null` result (the action set nothing) becomes undefined, so callers test one thing.\n const result = await this.postWithEnvelope<T | null>(\n `${this.baseUrl}/actions/execute`,\n body as any\n );\n return result ?? undefined;\n }\n\n // LookupReferences\n async getLookupReferences(): Promise<LookupReferenceListItem[]> {\n return firstValueFrom(this.http.get<LookupReferenceListItem[]>(`${this.baseUrl}/lookupref`));\n }\n\n async getLookupReference(name: string): Promise<LookupReference> {\n return firstValueFrom(this.http.get<LookupReference>(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`));\n }\n\n async addLookupReferenceValue(name: string, value: LookupReferenceValue): Promise<LookupReferenceValue> {\n return firstValueFrom(this.http.post<LookupReferenceValue>(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`, value));\n }\n\n async updateLookupReferenceValue(name: string, key: string, value: LookupReferenceValue): Promise<LookupReferenceValue> {\n return firstValueFrom(this.http.put<LookupReferenceValue>(\n `${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`,\n value\n ));\n }\n\n async deleteLookupReferenceValue(name: string, key: string): Promise<void> {\n return firstValueFrom(this.http.delete<void>(\n `${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`\n ));\n }\n\n /**\n * Posts to an add-on package's endpoint under the Spark base URL (`/spark` + `path`) and unwraps\n * the `ClientOperationEnvelope` exactly like the built-in mutations: operations are dispatched and\n * a 449 retry opens the retry modal. For the ng-spark add-on entry points (`soft-delete`,\n * `history`, `moderation`) whose server packages answer through core's envelope helpers\n * (`SparkAddOnEndpoints`, #460).\n */\n postEnvelope<T>(path: string, body: Record<string, unknown>): Promise<T> {\n return this.postWithEnvelope<T>(`${this.baseUrl}${path}`, body as EnvelopeRequestBody);\n }\n\n // Envelope-aware HTTP helpers.\n // All mutation endpoints (Create / Update / Delete / Execute custom action) emit the\n // ClientOperationEnvelope { result, operations } shape. These helpers unwrap the envelope,\n // dispatch any non-retry operations, and translate 449 retry-operations into the existing\n // RetryActionService modal flow.\n\n private postWithEnvelope<T>(url: string, body: EnvelopeRequestBody): Promise<T> {\n return this.sendWithEnvelope<T>(\n () => firstValueFrom(this.http.post<ClientOperationEnvelope<T>>(url, body)),\n body,\n () => this.postWithEnvelope<T>(url, body),\n );\n }\n\n /**\n * A read: posts the request, and returns the response **as-is** rather than unwrapping an\n * envelope.\n *\n * The read endpoints moved from GET to POST so their hooks could prompt, but their success\n * responses are still bare — a `PersistentObject`, a `QueryResult`. Only the 449 is enveloped,\n * because a retry has nowhere else to live. So this shares the retry loop with the mutating calls\n * and nothing else.\n */\n private async sendRead<T>(url: string, body: Record<string, unknown>): Promise<T> {\n try {\n return await firstValueFrom(this.http.post<T>(url, body));\n } catch (error) {\n return this.handleEnvelopeRetryError<T>(\n error as HttpErrorResponse,\n () => this.sendRead<T>(url, body),\n body as { retryResults?: RetryActionResult[] },\n );\n }\n }\n\n private async sendWithEnvelope<T>(\n send: () => Promise<ClientOperationEnvelope<T>>,\n body: { retryResults?: RetryActionResult[] },\n retryFn: () => Promise<T>,\n ): Promise<T> {\n try {\n const envelope = await send();\n if (envelope?.operations?.length) {\n this.dispatcher.dispatch(envelope.operations);\n }\n return envelope?.result as T;\n } catch (error) {\n return this.handleEnvelopeRetryError<T>(error as HttpErrorResponse, retryFn, body);\n }\n }\n\n private async handleEnvelopeRetryError<T>(\n error: HttpErrorResponse,\n retryFn: () => Promise<T>,\n body: { retryResults?: RetryActionResult[] }\n ): Promise<T> {\n if (error.status !== 449) throw error;\n const envelope = error.error as ClientOperationEnvelope<T> | undefined;\n if (!envelope?.operations?.length) throw error;\n\n // Dispatch any non-retry operations accumulated before the retry throw\n // so notify/refresh/etc. fire BEFORE the retry modal opens.\n const nonRetry = envelope.operations.filter(o => o.type !== 'retry');\n if (nonRetry.length) this.dispatcher.dispatch(nonRetry);\n\n const retryOp = envelope.operations.find(o => o.type === 'retry') as RetryOperation | undefined;\n if (!retryOp) throw error;\n\n // ⚠️ A hook that raises a retry WITHOUT first checking `Retry.Result` re-raises on every\n // resubmission, and this loop has no natural end: the modal reopens, the user answers, the\n // server asks again. Each round trip looks individually reasonable, so the tab spins until it is\n // closed and nothing is logged. The trap is documented in RetryFromEveryHookTests, and the .NET\n // client has had a bound since M3; this is the browser's.\n //\n // The bound is on ANSWERS ALREADY GIVEN, which is what `retryResults` is — so it counts the\n // conversation and not the component's lifetime, and two unrelated conversations do not add up.\n if ((body.retryResults?.length ?? 0) >= SparkService.MAX_RETRY_DEPTH) {\n throw new Error(\n `Gave up after answering ${SparkService.MAX_RETRY_DEPTH} retry prompts; the last was step ` +\n `${retryOp.step} (\"${retryOp.title}\"). A hook that raises a retry without checking ` +\n `Retry.Result first will do this — it re-raises on every resubmission.`);\n }\n\n const payload: RetryActionPayload = {\n type: 'retry-action',\n step: retryOp.step,\n title: retryOp.title,\n options: retryOp.options,\n defaultOption: retryOp.defaultOption ?? undefined,\n persistentObject: retryOp.persistentObject ?? undefined,\n message: retryOp.message ?? undefined,\n };\n const result = await this.retryActionService.show(payload);\n if (result.option === 'Cancel' && !payload.options.includes('Cancel')) throw error;\n\n body.retryResults = [...(body.retryResults || []), result];\n return retryFn();\n }\n}\n\n/**\n * The options every query execution takes.\n *\n * Extracted because `executeQueryByName` forwards to `executeQuery`, and while both spelled their\n * options as separate inline literals a field added to one silently failed to forward from the\n * other — which is how skip/take/search were once dropped for every reference picker.\n */\nexport interface ExecuteQueryOptions {\n sortColumns?: SortColumn[];\n parentId?: string;\n parentType?: string;\n skip?: number;\n take?: number;\n search?: string;\n /** Per-column value filters (#431). Columns AND together; values within a column OR. */\n columns?: QueryColumnFilter[];\n /**\n * Soft-deletion mode (#460): `exclude` (default), `include`, `only`. Sent only when set; the server\n * honours a widening only for callers holding `ViewDeleted` on the type.\n */\n deleted?: SparkDeletedFilter;\n /**\n * The mode the PARENT (`parentId`/`parentType`) is resolved under (#460) — `include` for a sub-query\n * on a deleted object's page, opened from the recycle bin. Independent of `deleted`, which stays the\n * rows' filter. Honoured only for callers holding `ViewDeleted` on the parent's type; anyone else\n * gets the same 404 a missing parent gives.\n */\n parentDeleted?: SparkDeletedFilter;\n}\n","/** Built-in SVG icons used by ng-spark library templates. */\nexport const SPARK_BUILT_IN_ICONS: Record<string, string> = {\n 'three-dots-vertical': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-three-dots-vertical\" viewBox=\"0 0 16 16\"><path d=\"M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0\"/></svg>',\n 'x-circle': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-circle\" viewBox=\"0 0 16 16\"><path d=\"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16\"/><path d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708\"/></svg>',\n 'arrow-left': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-left\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8\"/></svg>',\n 'pencil': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-pencil\" viewBox=\"0 0 16 16\"><path d=\"M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325\"/></svg>',\n 'plus': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus\" viewBox=\"0 0 16 16\"><path d=\"M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4\"/></svg>',\n 'plus-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus-lg\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2\"/></svg>',\n 'search': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-search\" viewBox=\"0 0 16 16\"><path d=\"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0\"/></svg>',\n 'trash': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-trash\" viewBox=\"0 0 16 16\"><path d=\"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z\"/><path d=\"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z\"/></svg>',\n 'x-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-lg\" viewBox=\"0 0 16 16\"><path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z\"/></svg>',\n};\n","import { Injectable, inject } from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { SPARK_BUILT_IN_ICONS } from './spark-built-in-icons';\n\n/**\n * R2-M12: SVG icon registry. The registry no longer accepts a raw string —\n * apps must pre-sanitize via `DomSanitizer.bypassSecurityTrustHtml` (or\n * `parseSvg` to validate) before calling `register`. Treating the parameter\n * as `SafeHtml` puts the explicit bypass decision in the caller's hands so a\n * developer wiring server-supplied SVG (per-tenant branding fetched from a\n * JSON endpoint) doesn't accidentally trust a `<svg><script>...</script>`\n * payload.\n *\n * The built-in icons baked into the package are still bypass-trusted internally\n * because they're known-safe at build time.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkIconRegistry {\n private sanitizer = inject(DomSanitizer);\n private icons = new Map<string, SafeHtml>();\n\n constructor() {\n for (const [name, svg] of Object.entries(SPARK_BUILT_IN_ICONS)) {\n this.icons.set(name, this.sanitizer.bypassSecurityTrustHtml(svg));\n }\n }\n\n /**\n * Registers a pre-sanitized SVG icon. Callers MUST validate or explicitly\n * trust the SVG before passing — for example:\n *\n * ```ts\n * const safe = sanitizer.bypassSecurityTrustHtml(svgFromBuildTimeConstant);\n * registry.register('app-logo', safe);\n * ```\n *\n * For server-supplied SVG, prefer a strict allow-list parser (no `<script>`,\n * no `on*` attributes, no `href=\"javascript:...\"`) before trusting.\n */\n register(name: string, svg: SafeHtml): void {\n this.icons.set(name, svg);\n }\n\n get(name: string): SafeHtml | undefined {\n return this.icons.get(name);\n }\n\n has(name: string): boolean {\n return this.icons.has(name);\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport { CustomActionDefinition, PersistentObject, SparkQuery, EntityType, filterQueryActions } from '@mintplayer/ng-spark/models';\nimport { SparkService } from './spark.service';\n\n/**\n * A query's custom actions, reachable **without the grid that usually renders them**.\n *\n * ## Why this exists\n *\n * A query's actions were only ever obtainable by rendering `<spark-query-grid>`, which loads the\n * query, resolves its entity type, fetches the actions and filters them to the ones marked for a\n * query surface — all privately. A page that wanted the same buttons somewhere else (a toolbar\n * above the card, the shell's topbar) had three bad options: duplicate the four-step resolution\n * and let it drift, reach into the grid's internals, or put the button in the wrong place.\n *\n * This exposes the resolution itself. The grid keeps rendering the default placement; a host that\n * wants a different one asks here and renders its own control.\n *\n * ## What it does not do\n *\n * It does not authorize. `/spark/actions/{type}` returns only the actions this caller may see —\n * the server filters against `security.json`, and executing re-checks — so nothing here is a gate,\n * and a host must not treat \"the list came back empty\" as anything other than a display fact.\n *\n * It also does not cache. Actions depend on the caller, and a memo keyed by type id would survive\n * a sign-out.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkQueryActionsService {\n private readonly sparkService = inject(SparkService);\n\n /**\n * The custom actions of the query named by id or alias, already filtered to those that belong on\n * a query surface.\n *\n * Returns an empty list — rather than throwing — when the query resolves to no entity type,\n * because a caller rendering a toolbar wants \"no buttons\", not a broken page.\n */\n async actionsFor(queryIdOrAlias: string): Promise<CustomActionDefinition[]> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) return [];\n\n // filterQueryActions, not a local predicate: the grid uses it too, and the `showedOn` values\n // are exactly the kind of thing a second copy gets subtly wrong — both grids once tested for\n // \"list\", a value nothing emits, and every correctly authored action rendered nowhere.\n return filterQueryActions(await this.sparkService.getCustomActions(context.entityType.id));\n }\n\n /**\n * Runs one of those actions. `selectedItemIds` are row ids, exactly as the grid posts them —\n * the server re-materializes each one through the same load path a detail page uses, so an id\n * from anywhere is treated as caller input rather than as a verified row.\n */\n async execute(\n queryIdOrAlias: string,\n actionName: string,\n options?: {\n parent?: PersistentObject;\n selectedItemIds?: string[];\n /** For a sub-query: the object whose detail page it was rendered on. See SparkService. */\n queryParent?: { id: string; type: string };\n },\n ): Promise<void> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) {\n throw new Error(\n `Cannot execute '${actionName}': query '${queryIdOrAlias}' resolves to no entity type, so there ` +\n `is nothing to execute it against.`);\n }\n\n await this.sparkService.executeCustomAction(\n context.entityType.id, actionName, options?.parent, options?.selectedItemIds, options?.queryParent,\n context.query.id);\n }\n\n /**\n * The query and the entity type its rows are mapped against — the two-step resolution both\n * methods need, kept in one place so they cannot disagree about which type an action runs on.\n */\n private async contextFor(\n queryIdOrAlias: string,\n ): Promise<{ query: SparkQuery; entityType: EntityType } | null> {\n const [query, entityTypes] = await Promise.all([\n this.sparkService.getQuery(queryIdOrAlias),\n this.sparkService.getEntityTypes(),\n ]);\n if (!query) return null;\n\n const entityType = entityTypes.find(t => t.name === query.entityType)\n ?? entityTypes.find(t => t.id === query.entityType);\n return entityType ? { query, entityType } : null;\n }\n}\n","import { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { HttpFeature, HttpFeatureKind, HttpInterceptorFn, withInterceptors } from '@angular/common/http';\nimport { PLATFORM_ID, inject } from '@angular/core';\n\n/**\n * The header carrying the viewer's IANA timezone id, e.g. `Europe/Brussels`.\n * Must stay in lockstep with `RequestTimeZoneResolver.HeaderName` on the server.\n */\nexport const SPARK_TIMEZONE_HEADER = 'X-Spark-Timezone';\n\n/**\n * The default name of the cookie carrying the same id, for requests the browser makes without any\n * script running first (a server-side render, a link opened from a mail). Must stay in lockstep\n * with `SparkTimeZoneOptions.DefaultCookieName` (`Spark:TimeZone:CookieName`) on the server.\n */\nexport const SPARK_TIMEZONE_COOKIE = 'spark-timezone';\n\n/** Options for {@link withSparkTimezone}. */\nexport interface SparkTimezoneOptions {\n /**\n * The cookie to write the viewer's zone into; `false` writes none (the header is still sent).\n * Defaults to {@link SPARK_TIMEZONE_COOKIE}. Use the same name as the server's\n * `Spark:TimeZone:CookieName`.\n */\n cookieName?: string | false;\n}\n\n/** One year: the zone only changes when the viewer travels, and each visit refreshes it. */\nconst COOKIE_MAX_AGE_SECONDS = 31_536_000;\n\n/** The server's shape check (`RequestTimeZoneResolver`): an id failing it would be ignored anyway. */\nconst ZONE_ID_SHAPE = /^[A-Za-z][A-Za-z0-9_+\\-]*(\\/[A-Za-z0-9_+\\-]+){0,2}$/;\n\n/**\n * Returns the browser's IANA zone id, or `null` when the environment cannot name one.\n *\n * An id is deliberately sent rather than a numeric offset: an offset is only valid at one instant,\n * so a server asked to render \"next Tuesday at 09:00\" from an offset captured today gets it wrong\n * across a DST boundary. A zone id carries the rules, not one sample of them.\n */\nexport function resolveBrowserTimeZone(): string | null {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || null;\n } catch {\n return null;\n }\n}\n\n/** The value of cookie `name` in `cookies` (`document.cookie`), or `null`. */\nfunction readCookie(cookies: string, name: string): string | null {\n for (const part of cookies.split(';')) {\n const eq = part.indexOf('=');\n if (eq > 0 && part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return null;\n}\n\n/**\n * Writes `zone` into cookie `name` unless it already holds it. Browser only; the server never\n * writes this cookie. `Secure` on https, `SameSite=Lax` so a link from a mail still carries it.\n */\nfunction writeTimezoneCookie(doc: Document, name: string, zone: string): void {\n if (readCookie(doc.cookie, name) === zone) return;\n const secure = doc.location?.protocol === 'https:' ? '; Secure' : '';\n doc.cookie = `${name}=${zone}; Path=/; Max-Age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax${secure}`;\n}\n\n/**\n * Builds the interceptor for {@link withSparkTimezone}. Exported for apps that compose their own\n * `withInterceptors([...])`.\n *\n * **This does not participate in reading or writing timestamps.** A `DateTimeOffset` means an\n * instant, and the browser converts in both directions on those paths. The header and cookie exist\n * for work the server does on its own — a scheduled export, a notification email, a server-side\n * render — where there is no browser in the loop to do the converting but the output still has to\n * read as local time.\n *\n * ⚠️ **Nothing happens on the server platform.** During a server-side render `Intl` names the Node\n * process's zone (UTC in a container), and a header carrying it would win over the viewer's cookie\n * on the server (spike S-TZ3, #460).\n */\nexport function createSparkTimezoneInterceptor(options: SparkTimezoneOptions = {}): HttpInterceptorFn {\n const cookieName = options.cookieName === undefined ? SPARK_TIMEZONE_COOKIE : options.cookieName;\n\n return (req, next) => {\n if (!isPlatformBrowser(inject(PLATFORM_ID))) return next(req);\n\n const zone = resolveBrowserTimeZone();\n if (!zone) return next(req);\n\n if (cookieName && ZONE_ID_SHAPE.test(zone)) {\n writeTimezoneCookie(inject(DOCUMENT), cookieName, zone);\n }\n\n // Never annotate a cross-origin call: the viewer's timezone is a (small) fingerprinting signal\n // and third-party hosts have no business receiving it.\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(req.url)) {\n try {\n if (new URL(req.url).origin !== window.location.origin) return next(req);\n } catch {\n return next(req);\n }\n }\n\n return next(req.clone({ setHeaders: { [SPARK_TIMEZONE_HEADER]: zone } }));\n };\n}\n\n/**\n * Attaches {@link SPARK_TIMEZONE_HEADER} to same-origin requests and keeps the\n * {@link SPARK_TIMEZONE_COOKIE} cookie current — with the default options. See\n * {@link createSparkTimezoneInterceptor}.\n */\nexport const sparkTimezoneInterceptor: HttpInterceptorFn = createSparkTimezoneInterceptor();\n\n/**\n * `provideHttpClient(...withSparkTimezone())` — registers the timezone interceptor.\n *\n * Spreadable alongside `withSparkAuth()`:\n * `provideHttpClient(...withSparkAuth(), ...withSparkTimezone())`.\n *\n * The cookie is written on the first request the browser app makes, so the very first server-side\n * render of a first visit has no cookie and renders in UTC.\n */\nexport function withSparkTimezone(options?: SparkTimezoneOptions): HttpFeature<HttpFeatureKind>[] {\n return [withInterceptors([options ? createSparkTimezoneInterceptor(options) : sparkTimezoneInterceptor])];\n}\n","import { inject, Injectable } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { SparkService } from './spark.service';\n\n/** The `history.state` key a list row link stores the list's own URL under. */\nexport const SPARK_RETURN_URL_STATE_KEY = 'sparkReturnUrl';\n\n/**\n * Where a detail page goes back to after its row stops existing — a Delete, or a Purge from the\n * recycle bin (#460).\n *\n * Neither \"the start page\" nor \"the previous history entry\" is the right answer: the first loses the\n * user's place, the second may be the deleted row's edit page, or another site. The right answer is\n * the list the row was opened from, so a list's row link records that list's URL in the navigation\n * state ({@link stateForRowLink}); a detail page reached otherwise (a bookmark, a reference link)\n * falls back to the row type's own list — its program-unit query, else any query over the type.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkReturnNavigationService {\n private readonly router = inject(Router);\n private readonly spark = inject(SparkService);\n\n /** Navigation state for a row link: the current URL (the list, with its `?deleted=` mode). */\n stateForRowLink(): Record<string, string> {\n return { [SPARK_RETURN_URL_STATE_KEY]: this.router.url };\n }\n\n /** The list URL the current detail page was opened from, when a row link recorded one. */\n recordedReturnUrl(): string | null {\n const state = (typeof history !== 'undefined' ? history.state : null) as Record<string, unknown> | null;\n const url = state?.[SPARK_RETURN_URL_STATE_KEY];\n // Only an in-app path: the state is the app's own, but a protocol-relative \"//host\" is not a path.\n return typeof url === 'string' && url.startsWith('/') && !url.startsWith('//') ? url : null;\n }\n\n /**\n * Navigates to the list the row came from, else to the type's list (`?deleted=only` kept when\n * `deletedView`), else to the start page. `replaceUrl`: the gone row's page is not worth a Back.\n */\n async returnToList(entityTypeName: string | null | undefined, options?: { deletedView?: boolean }): Promise<void> {\n const recorded = this.recordedReturnUrl();\n if (recorded) {\n await this.router.navigateByUrl(recorded, { replaceUrl: true });\n return;\n }\n const route = entityTypeName ? await this.listRouteFor(entityTypeName) : null;\n if (route) {\n await this.router.navigate(route, {\n replaceUrl: true,\n ...(options?.deletedView ? { queryParams: { deleted: 'only' } } : {}),\n });\n return;\n }\n await this.router.navigate(['/'], { replaceUrl: true });\n }\n\n /** `['/query', alias]` of the type's list: a menu (program-unit) query first, else any query. */\n async listRouteFor(entityTypeName: string): Promise<unknown[] | null> {\n let queries;\n try {\n queries = (await this.spark.getQueries()).filter(q => q.entityType === entityTypeName);\n } catch {\n return null;\n }\n if (!queries.length) return null;\n try {\n const units = (await this.spark.getProgramUnits()).programUnitGroups\n .flatMap(g => g.programUnits)\n .filter(u => u.type === 'query')\n .sort((a, b) => a.order - b.order);\n for (const unit of units) {\n if (queries.some(q => q.id === unit.queryId || (!!unit.alias && q.alias === unit.alias)))\n return ['/query', unit.alias || unit.queryId!];\n }\n } catch {\n // No menu: any query over the type still beats the start page.\n }\n return ['/query', queries[0].alias || queries[0].id];\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mintplayer-ng-spark-services.mjs","names":[],"sources":["../../services/src/retry-action.service.ts","../../services/src/spark-language.service.ts","../../services/src/spark-streaming.service.ts","../../services/src/spark.service.ts","../../services/src/spark-built-in-icons.ts","../../services/src/spark-icon-registry.ts","../../services/src/spark-query-actions.service.ts","../../services/src/spark-timezone.interceptor.ts","../../services/src/spark-return-navigation.service.ts"],"sourcesContent":["import { Injectable, signal } from '@angular/core';\nimport { RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\n\n@Injectable({ providedIn: 'root' })\nexport class RetryActionService {\n private resolveRetry: ((result: RetryActionResult) => void) | null = null;\n\n payload = signal<RetryActionPayload | null>(null);\n\n show(payload: RetryActionPayload): Promise<RetryActionResult> {\n this.payload.set(payload);\n return new Promise(resolve => { this.resolveRetry = resolve; });\n }\n\n respond(result: RetryActionResult): void {\n this.payload.set(null);\n this.resolveRetry?.(result);\n this.resolveRetry = null;\n }\n}\n","import { Injectable, inject, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { TranslatedString, currentLanguage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\ninterface CultureConfiguration {\n languages: Record<string, TranslatedString>;\n defaultLanguage: string;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class SparkLanguageService {\n private readonly http = inject(HttpClient);\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly currentLang = signal('en');\n private readonly translationsMap = signal<Record<string, TranslatedString>>({});\n\n readonly language = this.currentLang.asReadonly();\n readonly languages = signal<Record<string, TranslatedString>>({});\n\n constructor() {\n this.loadCulture();\n this.loadTranslations();\n }\n\n private async loadCulture(): Promise<void> {\n const config = await firstValueFrom(this.http.get<CultureConfiguration>(`${this.baseUrl}/culture`));\n this.languages.set(config.languages);\n const saved = localStorage.getItem('spark-lang');\n const lang = saved ?? config.defaultLanguage;\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n }\n\n private async loadTranslations(): Promise<void> {\n const t = await firstValueFrom(this.http.get<Record<string, TranslatedString>>(`${this.baseUrl}/translations`));\n this.translationsMap.set(t);\n }\n\n setLanguage(lang: string) {\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n localStorage.setItem('spark-lang', lang);\n }\n\n resolve(ts: TranslatedString | undefined): string {\n if (!ts) return '';\n const lang = this.currentLang();\n return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n }\n\n t(key: string): string {\n const ts = this.translationsMap()[key];\n return this.resolve(ts) || key;\n }\n}\n","import { inject, Injectable, NgZone } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { StreamingMessage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n@Injectable({ providedIn: 'root' })\nexport class SparkStreamingService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly ngZone = inject(NgZone);\n\n connectToStreamingQuery(queryId: string): Observable<StreamingMessage> {\n return new Observable<StreamingMessage>(subscriber => {\n let ws: WebSocket | null = null;\n let retryCount = 0;\n let retryTimeout: ReturnType<typeof setTimeout> | null = null;\n let closed = false;\n const maxRetries = 10;\n const maxDelay = 30000;\n\n const connect = () => {\n if (closed) return;\n\n const url = this.buildWebSocketUrl(queryId);\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n retryCount = 0; // Reset on successful connect\n };\n\n ws.onmessage = (event) => {\n try {\n const message: StreamingMessage = JSON.parse(event.data);\n this.ngZone.run(() => subscriber.next(message));\n } catch {\n // Ignore malformed messages\n }\n };\n\n ws.onerror = () => {\n // Error will trigger onclose\n };\n\n ws.onclose = (event) => {\n if (closed) return;\n\n // Don't reconnect on normal closure\n if (event.code === 1000) {\n this.ngZone.run(() => subscriber.complete());\n return;\n }\n\n // Reconnect with exponential backoff\n if (retryCount < maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, retryCount), maxDelay);\n retryCount++;\n retryTimeout = setTimeout(() => connect(), delay);\n } else {\n this.ngZone.run(() => subscriber.error(new Error('WebSocket connection failed after maximum retries')));\n }\n };\n };\n\n connect();\n\n // Teardown: close WebSocket on unsubscribe\n return () => {\n closed = true;\n if (retryTimeout) {\n clearTimeout(retryTimeout);\n }\n if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {\n ws.close(1000, 'Client unsubscribed');\n }\n };\n });\n }\n\n private buildWebSocketUrl(queryId: string): string {\n const encodedId = encodeURIComponent(queryId);\n const path = `${this.baseUrl}/queries/${encodedId}/stream`;\n\n // If baseUrl is absolute (starts with http/https), replace protocol\n if (this.baseUrl.startsWith('http://')) {\n return path.replace(/^http:\\/\\//, 'ws://');\n }\n if (this.baseUrl.startsWith('https://')) {\n return path.replace(/^https:\\/\\//, 'wss://');\n }\n\n // Relative URL — construct from window.location\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${window.location.host}${path}`;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { HttpClient, HttpErrorResponse } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { CustomActionDefinition, DistinctValuesResult, EntityPermissions, EntityType, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, ProgramUnitsConfiguration, QueryColumnFilter, QueryResult, SparkDeletedFilter, SparkQuery, RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\nimport { ClientOperationEnvelope, RetryOperation, SparkClientOperationDispatcher } from '@mintplayer/ng-spark/client-operations';\nimport { SortColumn } from '@mintplayer/pagination';\nimport { RetryActionService } from './retry-action.service';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n/**\n * Context for {@link SparkService.newObject}. Everything is optional: a standalone New sends an\n * empty body, and an AsDetail row on a parent that has never been saved sends no `parentId` —\n * which is the honest answer, not a gap. The server hands the hook a null parent rather than\n * trusting the client's copy of one.\n */\nexport interface NewObjectOptions {\n /** Name of the parent's AsDetail attribute the row is for; absent for a standalone New. */\n asDetailAttribute?: string;\n /** The parent's entity type — required whenever `asDetailAttribute` is set. */\n parentType?: string;\n /** The parent's id, absent while the parent is itself unsaved. */\n parentId?: string;\n /** Free-form arguments, e.g. which variant a New menu chose. */\n parameters?: Record<string, string>;\n /**\n * The sub-query New was started from (#460, D19), with `parentType` and `parentId` and no\n * `asDetailAttribute`. The server loads the parent, checks the parent's type declares this\n * sub-query, and runs `OnNewAsync` with it — whose base fills the reference to the parent.\n */\n queryId?: string;\n}\n\n/** Where a bulk delete was started from; see {@link SparkService.deleteMany}. */\nexport interface DeleteManyOptions {\n /**\n * The query the rows were selected in. Required (#467, D12): the server fetches the rows through it\n * and applies its `OnDisableActionsAsync` decision; without it the call is a 400.\n */\n queryId: string;\n /** The sub-query's container, when deleting from a sub-query. */\n parentId?: string;\n parentType?: string;\n /** One reason for the whole batch, recorded on every soft-deleted row (#467, D20). */\n reason?: string;\n}\n\n/** A row and the version of it the caller saw (#467, D14): what a bulk delete names. */\nexport interface SparkRowVersion {\n id: string;\n /** The row's `etag` from the query result. */\n etag: string;\n}\n\n/** Context for {@link SparkService.deleteRow}. Every field is required — see `DeleteRow.cs`. */\nexport interface DeleteRowOptions {\n asDetailAttribute: string;\n parentType: string;\n parentId: string;\n /** The row's `__sparkRowKey`, as it arrived from the server. */\n rowKey: string;\n parameters?: Record<string, string>;\n}\n\n/**\n * The union of every body the envelope helpers send.\n *\n * Named rather than inlined per helper because the New and delete-row calls carry neither a\n * `persistentObject` nor a `triggeredBy`: an inline type that did not list their fields would have\n * rejected them at the call site, and spreading them through `any` would have hidden a typo in a\n * field name the server matches by exact spelling.\n */\ntype EnvelopeRequestBody = {\n /** The entity type the request is about — the request parameter, not the submitted object's claim. */\n objectTypeId?: string;\n /** The target object's id, for update and delete. */\n id?: string;\n persistentObject?: any;\n triggeredBy?: string;\n retryResults?: RetryActionResult[];\n /** The version being deleted, for a single delete. */\n etag?: string;\n /** The rows of a bulk delete, each with its version. */\n items?: SparkRowVersion[];\n} & Partial<NewObjectOptions> & Partial<DeleteManyOptions> & Partial<Omit<DeleteRowOptions, 'asDetailAttribute' | 'parentType' | 'parentId'>>;\n\n@Injectable({ providedIn: 'root' })\nexport class SparkService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly http = inject(HttpClient);\n private readonly retryActionService = inject(RetryActionService);\n\n /**\n * How many prompts one request will answer before giving up. Matches the .NET client's\n * `SparkClient.MaxRetryDepth`, and for the same reason: this is a termination bound, not a\n * capacity one. Far above any real conversation (Fleet's longest is two) and far below anything\n * that could hide a spin.\n */\n private static readonly MAX_RETRY_DEPTH = 16;\n private readonly dispatcher = inject(SparkClientOperationDispatcher);\n\n // Entity Types\n async getEntityTypes(): Promise<EntityType[]> {\n return firstValueFrom(this.http.get<EntityType[]>(`${this.baseUrl}/types`));\n }\n\n async getEntityType(id: string): Promise<EntityType> {\n return firstValueFrom(this.http.get<EntityType>(`${this.baseUrl}/types/${encodeURIComponent(id)}`));\n }\n\n async getEntityTypeByClrType(clrType: string): Promise<EntityType | undefined> {\n const types = await this.getEntityTypes();\n return types.find(t => t.clrType === clrType);\n }\n\n // Permissions\n async getPermissions(entityTypeId: string): Promise<EntityPermissions> {\n return firstValueFrom(this.http.get<EntityPermissions>(`${this.baseUrl}/permissions/${encodeURIComponent(entityTypeId)}`));\n }\n\n /** Resolved at most once; see getQueryByName. */\n #queryCatalogue?: Promise<SparkQuery[]>;\n\n // Queries\n async getQueries(): Promise<SparkQuery[]> {\n return firstValueFrom(this.http.get<SparkQuery[]>(`${this.baseUrl}/queries`));\n }\n\n async getQuery(id: string): Promise<SparkQuery> {\n return firstValueFrom(this.http.post<SparkQuery>(`${this.baseUrl}/queries/get`, { queryId: id }));\n }\n\n /**\n * Resolves a query by name, fetching the catalogue at most once per service instance.\n *\n * Without the cache this costs a full `GET /spark/queries` per call, and a form with several\n * reference attributes resolves them in parallel — so opening one page fetched the whole query\n * catalogue N times over. The catalogue is per-caller and effectively static for a page's\n * lifetime; the service is scoped to the app, so a permission change lands on the next load.\n */\n async getQueryByName(name: string): Promise<SparkQuery | undefined> {\n this.#queryCatalogue ??= this.getQueries();\n const queries = await this.#queryCatalogue;\n return queries.find(q => q.name === name);\n }\n\n async executeQuery(queryId: string, options?: ExecuteQueryOptions): Promise<QueryResult> {\n // A POST with a typed body, not a GET with a query string. `sortColumns` used to be encoded as\n // `prop:asc,other:desc` — an encoding invented because a query string has no arrays. Column\n // filtering (several columns, several selected values each) would need a second such encoding,\n // which is the reason the reads moved to POST at all.\n return this.sendRead<QueryResult>(`${this.baseUrl}/queries/execute`, {\n queryId,\n sortColumns: options?.sortColumns?.length\n ? options.sortColumns.map(c => ({ property: c.property, direction: c.direction === 'descending' ? 'desc' : 'asc' }))\n : undefined,\n parentId: options?.parentId,\n parentType: options?.parentType,\n skip: options?.skip,\n take: options?.take,\n search: options?.search,\n // Per-column value filters (#431). The shape that could not be expressed as a query string,\n // and the reason these reads are a POST at all.\n columns: options?.columns?.length ? options.columns : undefined,\n // Soft-deletion mode (#460, T2). Omitted = exclude.\n deleted: options?.deleted,\n // The parent's own mode (#460): a deleted parent opened from the recycle bin. Omitted = exclude.\n parentDeleted: options?.parentDeleted,\n });\n }\n\n /**\n * The distinct values behind one column's filter panel (#431).\n *\n * `columns` carries the OTHER columns' current filters, so the values returned are the ones still\n * reachable — offering a value that empties the grid the moment it is picked is worse than\n * omitting it.\n *\n * An empty result means either \"nothing matches\" or \"you may not enumerate this column\". The two\n * are deliberately indistinguishable, so there is nothing for a caller to branch on.\n */\n async getDistinctValues(queryId: string, column: string, options?: {\n /** Narrows the listed values themselves (the panel's own box). */\n search?: string;\n /**\n * The grid's search term, what `executeQuery` sends as `search` (#460 M15): the values come only\n * from the rows it matches, so a searched grid's panel lists what the grid shows.\n */\n querySearch?: string;\n columns?: QueryColumnFilter[];\n parentId?: string;\n parentType?: string;\n /** The same soft-deletion mode as the grid the panel belongs to (#460). */\n deleted?: SparkDeletedFilter;\n /** The same parent mode as the grid the panel belongs to (#460). */\n parentDeleted?: SparkDeletedFilter;\n }): Promise<DistinctValuesResult> {\n return this.sendRead<DistinctValuesResult>(`${this.baseUrl}/queries/distinct-values`, {\n queryId,\n column,\n search: options?.search || undefined,\n querySearch: options?.querySearch || undefined,\n columns: options?.columns?.length ? options.columns : undefined,\n deleted: options?.deleted,\n parentId: options?.parentId,\n parentType: options?.parentType,\n parentDeleted: options?.parentDeleted,\n });\n }\n\n /**\n * Runs a query identified by name.\n *\n * It used to forward only the parent, dropping skip/take/search — so every reference picker and\n * AsDetail reference column silently saw the server's default of 50 candidates, with no indication\n * that the list was cut. A picker that omits the option you are looking for, and says nothing, is\n * worse than one that fails.\n */\n async executeQueryByName(queryName: string, options?: ExecuteQueryOptions): Promise<QueryResult> {\n const query = await this.getQueryByName(queryName);\n if (!query) return { columns: [], items: [], totalItems: 0, skip: 0, take: options?.take ?? 50 };\n return this.executeQuery(query.id, options);\n }\n\n // Program Units\n async getProgramUnits(): Promise<ProgramUnitsConfiguration> {\n return firstValueFrom(this.http.get<ProgramUnitsConfiguration>(`${this.baseUrl}/program-units`));\n }\n\n // Persistent Objects\n /**\n * Loads one object. `options.deleted` (#460) lets a `ViewDeleted` holder open a soft-deleted row\n * from the recycle bin (`'include'` or `'only'`); without the right the server ignores it and a\n * deleted row stays a 404. Omitted = exclude.\n */\n async get(type: string, id: string, options?: { deleted?: SparkDeletedFilter }): Promise<PersistentObject> {\n return this.sendRead<PersistentObject>(`${this.baseUrl}/po/load`, { objectTypeId: type, id, deleted: options?.deleted });\n }\n\n /**\n * `parent` is the sub-query the New was started from — the same `parentId`/`parentType`/`queryId`\n * `newObject` was given. The server resolves and authorizes it again and hands it to the save hooks\n * as `PersistentObject.Parent`.\n *\n * Resolves to `null` when a server interceptor cancelled the create (#482: the user answered a prompt with\n * Cancel) — the server answers 204 and nothing was created.\n */\n async create(\n type: string,\n data: Partial<PersistentObject>,\n parent?: { parentId: string; parentType: string; queryId: string },\n ): Promise<PersistentObject | null> {\n const created = await this.postWithEnvelope<PersistentObject | null>(\n `${this.baseUrl}/po/create`,\n { objectTypeId: type, persistentObject: data, ...(parent ?? {}) }\n );\n return created ?? null;\n }\n\n /**\n * Saves an edit. `data.etag` — the etag the object was loaded with — is required (#467, D16): it is\n * the version being edited. A 409 means the object changed since (`reason: 'changed'`) or was\n * deleted since (`reason: 'deleted'`).\n */\n async update(type: string, id: string, data: Partial<PersistentObject>): Promise<PersistentObject> {\n if (!data.etag) throw new Error(`An update of '${id}' must carry the etag it was loaded with.`);\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/update`,\n { objectTypeId: type, id, persistentObject: data }\n );\n }\n\n /**\n * Asks the server to reshape an in-progress object after `triggeredBy`'s value changed.\n *\n * Writes nothing, but goes through the envelope like every other mutating call: a refresh may\n * legitimately emit notifications, and may open the retry-action prompt.\n *\n * `triggeredBy` is the attribute's name. For a trigger inside an AsDetail row it is the same\n * path form the inline validation errors use — `Jobs[2].ProfessionId`.\n */\n async refresh(type: string, data: Partial<PersistentObject>, triggeredBy: string): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/refresh`,\n { objectTypeId: type, persistentObject: data, triggeredBy }\n );\n }\n\n /**\n * Asks the server to construct a new object of `type`, so `OnNewAsync` can default it.\n *\n * Writes nothing — the object comes back unsaved, and for an AsDetail row the parent still owns\n * the save. Called by the create page for every New (#460, D19: with `parentId`/`parentType`/\n * `queryId` when started from a sub-query, whose base hook fills the parent reference), and for an\n * AsDetail row only when its type's `serverSideRowLifecycle` is on; every other row type keeps\n * building its blank row locally, which is why switching the flag off costs no request.\n *\n * ⚠️ The row comes back **keyed**: the server constructs the CLR instance, so the row-key field\n * initializer runs and `po.id` carries the key. Flattening it with `nestedPoToDict` therefore\n * populates `__sparkRowKey`, and the row is matchable on save from the moment it is added — the\n * whole reason this round-trip is worth a request.\n */\n async newObject(type: string, options?: NewObjectOptions): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/new`,\n { objectTypeId: type, ...(options ?? {}) }\n );\n }\n\n /**\n * Asks whether a stored row may leave its parent's AsDetail collection.\n *\n * Deletes nothing: a resolved promise means the caller may splice the row out of the collection\n * it is editing, and the removal is persisted with the parent. A hook that refuses rejects with a\n * 400 whose `error.error.result.errors` carries the readable reason, exactly like a save.\n *\n * A POST — as everything is now. It used to be worth explaining: the framework's real delete route\n * was a catch-all that would have swallowed a sibling DELETE. The catch-all is gone with the rest\n * of the route variables.\n */\n async deleteRow(type: string, options: DeleteRowOptions): Promise<void> {\n await this.postWithEnvelope<{ removed: boolean }>(\n `${this.baseUrl}/po/delete-row`,\n { objectTypeId: type, ...options }\n );\n }\n\n /**\n * Deletes one object. `etag` is the version being deleted — the loaded object's `etag` — and is\n * required (#467, D14): an object changed since is a 409.\n */\n async delete(type: string, id: string, etag: string): Promise<void> {\n return this.postWithEnvelope<void>(\n `${this.baseUrl}/po/delete`,\n { objectTypeId: type, id, etag }\n );\n }\n\n /**\n * Deletes several rows of one type in one request — the default Delete action on a selection\n * (`POST /spark/po/delete-many`, #460 D18). Each row carries the `etag` its query result row had\n * (#467, D14). All or nothing: a 404 means some row is missing or not yours to delete, a 403 that\n * the server's `OnDisableActionsAsync` withholds Delete on the query or on one of the rows, a 409\n * that rows changed since the list loaded (the body's `message` names them), a 400 that the\n * selection breaks the rule or the 200-row cap.\n */\n async deleteMany(type: string, items: SparkRowVersion[], options: DeleteManyOptions): Promise<void> {\n return this.postWithEnvelope<void>(\n `${this.baseUrl}/po/delete-many`,\n { objectTypeId: type, items, ...options }\n );\n }\n\n // Custom Actions\n async getCustomActions(objectTypeId: string): Promise<CustomActionDefinition[]> {\n return firstValueFrom(this.http.post<CustomActionDefinition[]>(`${this.baseUrl}/actions/list`, { objectTypeId }));\n }\n\n /**\n * @param parent The object of THIS type the action is operating on — the detail-page invocation.\n * @param selectedItemIds Row ids from a query. Ids, never row objects: a row is a projection.\n * @param queryParent When invoked from a sub-query, the object whose detail page it was rendered\n * on — a DIFFERENT type (the cars listed on a company's page are Cars, the page is a Company).\n * Sent as id + type, exactly as the query endpoint names it, and resolved server-side under its\n * own type with its own Read gate.\n * @param queryId The query the selection came from, so the server can re-run it narrowed to those\n * ids and hand the action the rows the grid actually had -- index-computed columns included.\n * @returns What the action handed to `CustomActionArgs.SetResult` (#460, T5) -- the envelope's\n * `result` -- or `undefined` when it set nothing. The value is the action author's to shape: it\n * bypasses the server's redaction. A 403 means the action is disabled for this object, query or\n * selection (the server's `OnDisableActionsAsync` said so), distinct from a 404 for a row the\n * caller cannot see.\n */\n async executeCustomAction<T = unknown>(\n objectTypeId: string,\n actionName: string,\n parent?: PersistentObject,\n selectedItemIds?: string[],\n queryParent?: { id: string; type: string },\n queryId?: string,\n ): Promise<T | undefined> {\n const body: {\n objectTypeId: string; actionName: string;\n parent?: PersistentObject; selectedItemIds?: string[];\n parentId?: string; parentType?: string; queryId?: string;\n retryResults?: RetryActionResult[];\n } = { objectTypeId, actionName, parent, selectedItemIds, parentId: queryParent?.id, parentType: queryParent?.type, queryId };\n // A literal `null` result (the action set nothing) becomes undefined, so callers test one thing.\n const result = await this.postWithEnvelope<T | null>(\n `${this.baseUrl}/actions/execute`,\n body as any\n );\n return result ?? undefined;\n }\n\n // LookupReferences\n async getLookupReferences(): Promise<LookupReferenceListItem[]> {\n return firstValueFrom(this.http.get<LookupReferenceListItem[]>(`${this.baseUrl}/lookupref`));\n }\n\n async getLookupReference(name: string): Promise<LookupReference> {\n return firstValueFrom(this.http.get<LookupReference>(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`));\n }\n\n async addLookupReferenceValue(name: string, value: LookupReferenceValue): Promise<LookupReferenceValue> {\n return firstValueFrom(this.http.post<LookupReferenceValue>(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`, value));\n }\n\n async updateLookupReferenceValue(name: string, key: string, value: LookupReferenceValue): Promise<LookupReferenceValue> {\n return firstValueFrom(this.http.put<LookupReferenceValue>(\n `${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`,\n value\n ));\n }\n\n async deleteLookupReferenceValue(name: string, key: string): Promise<void> {\n return firstValueFrom(this.http.delete<void>(\n `${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`\n ));\n }\n\n /**\n * Posts to an add-on package's endpoint under the Spark base URL (`/spark` + `path`) and unwraps\n * the `ClientOperationEnvelope` exactly like the built-in mutations: operations are dispatched and\n * a 449 retry opens the retry modal. For the ng-spark add-on entry points (`soft-delete`,\n * `history`, `moderation`) whose server packages answer through core's envelope helpers\n * (`SparkAddOnEndpoints`, #460).\n */\n postEnvelope<T>(path: string, body: Record<string, unknown>): Promise<T> {\n return this.postWithEnvelope<T>(`${this.baseUrl}${path}`, body as EnvelopeRequestBody);\n }\n\n // Envelope-aware HTTP helpers.\n // All mutation endpoints (Create / Update / Delete / Execute custom action) emit the\n // ClientOperationEnvelope { result, operations } shape. These helpers unwrap the envelope,\n // dispatch any non-retry operations, and translate 449 retry-operations into the existing\n // RetryActionService modal flow.\n\n private postWithEnvelope<T>(url: string, body: EnvelopeRequestBody): Promise<T> {\n return this.sendWithEnvelope<T>(\n () => firstValueFrom(this.http.post<ClientOperationEnvelope<T>>(url, body)),\n body,\n () => this.postWithEnvelope<T>(url, body),\n );\n }\n\n /**\n * A read: posts the request, and returns the response **as-is** rather than unwrapping an\n * envelope.\n *\n * The read endpoints moved from GET to POST so their hooks could prompt, but their success\n * responses are still bare — a `PersistentObject`, a `QueryResult`. Only the 449 is enveloped,\n * because a retry has nowhere else to live. So this shares the retry loop with the mutating calls\n * and nothing else.\n */\n private async sendRead<T>(url: string, body: Record<string, unknown>): Promise<T> {\n try {\n return await firstValueFrom(this.http.post<T>(url, body));\n } catch (error) {\n return this.handleEnvelopeRetryError<T>(\n error as HttpErrorResponse,\n () => this.sendRead<T>(url, body),\n body as { retryResults?: RetryActionResult[] },\n );\n }\n }\n\n private async sendWithEnvelope<T>(\n send: () => Promise<ClientOperationEnvelope<T>>,\n body: { retryResults?: RetryActionResult[] },\n retryFn: () => Promise<T>,\n ): Promise<T> {\n try {\n const envelope = await send();\n if (envelope?.operations?.length) {\n this.dispatcher.dispatch(envelope.operations);\n }\n return envelope?.result as T;\n } catch (error) {\n return this.handleEnvelopeRetryError<T>(error as HttpErrorResponse, retryFn, body);\n }\n }\n\n private async handleEnvelopeRetryError<T>(\n error: HttpErrorResponse,\n retryFn: () => Promise<T>,\n body: { retryResults?: RetryActionResult[] }\n ): Promise<T> {\n if (error.status !== 449) throw error;\n const envelope = error.error as ClientOperationEnvelope<T> | undefined;\n if (!envelope?.operations?.length) throw error;\n\n // Dispatch any non-retry operations accumulated before the retry throw\n // so notify/refresh/etc. fire BEFORE the retry modal opens.\n const nonRetry = envelope.operations.filter(o => o.type !== 'retry');\n if (nonRetry.length) this.dispatcher.dispatch(nonRetry);\n\n const retryOp = envelope.operations.find(o => o.type === 'retry') as RetryOperation | undefined;\n if (!retryOp) throw error;\n\n // ⚠️ A hook that raises a retry WITHOUT first checking `Retry.Result` re-raises on every\n // resubmission, and this loop has no natural end: the modal reopens, the user answers, the\n // server asks again. Each round trip looks individually reasonable, so the tab spins until it is\n // closed and nothing is logged. The trap is documented in RetryFromEveryHookTests, and the .NET\n // client has had a bound since M3; this is the browser's.\n //\n // The bound is on ANSWERS ALREADY GIVEN, which is what `retryResults` is — so it counts the\n // conversation and not the component's lifetime, and two unrelated conversations do not add up.\n if ((body.retryResults?.length ?? 0) >= SparkService.MAX_RETRY_DEPTH) {\n throw new Error(\n `Gave up after answering ${SparkService.MAX_RETRY_DEPTH} retry prompts; the last was step ` +\n `${retryOp.step} (\"${retryOp.title}\"). A hook that raises a retry without checking ` +\n `Retry.Result first will do this — it re-raises on every resubmission.`);\n }\n\n const payload: RetryActionPayload = {\n type: 'retry-action',\n step: retryOp.step,\n title: retryOp.title,\n options: retryOp.options,\n defaultOption: retryOp.defaultOption ?? undefined,\n persistentObject: retryOp.persistentObject ?? undefined,\n message: retryOp.message ?? undefined,\n };\n const result = await this.retryActionService.show(payload);\n if (result.option === 'Cancel' && !payload.options.includes('Cancel')) throw error;\n\n body.retryResults = [...(body.retryResults || []), result];\n return retryFn();\n }\n}\n\n/**\n * The options every query execution takes.\n *\n * Extracted because `executeQueryByName` forwards to `executeQuery`, and while both spelled their\n * options as separate inline literals a field added to one silently failed to forward from the\n * other — which is how skip/take/search were once dropped for every reference picker.\n */\nexport interface ExecuteQueryOptions {\n sortColumns?: SortColumn[];\n parentId?: string;\n parentType?: string;\n skip?: number;\n take?: number;\n search?: string;\n /** Per-column value filters (#431). Columns AND together; values within a column OR. */\n columns?: QueryColumnFilter[];\n /**\n * Soft-deletion mode (#460): `exclude` (default), `include`, `only`. Sent only when set; the server\n * honours a widening only for callers holding `ViewDeleted` on the type.\n */\n deleted?: SparkDeletedFilter;\n /**\n * The mode the PARENT (`parentId`/`parentType`) is resolved under (#460) — `include` for a sub-query\n * on a deleted object's page, opened from the recycle bin. Independent of `deleted`, which stays the\n * rows' filter. Honoured only for callers holding `ViewDeleted` on the parent's type; anyone else\n * gets the same 404 a missing parent gives.\n */\n parentDeleted?: SparkDeletedFilter;\n}\n","/** Built-in SVG icons used by ng-spark library templates. */\nexport const SPARK_BUILT_IN_ICONS: Record<string, string> = {\n 'three-dots-vertical': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-three-dots-vertical\" viewBox=\"0 0 16 16\"><path d=\"M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0\"/></svg>',\n 'x-circle': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-circle\" viewBox=\"0 0 16 16\"><path d=\"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16\"/><path d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708\"/></svg>',\n 'arrow-left': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-left\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8\"/></svg>',\n 'pencil': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-pencil\" viewBox=\"0 0 16 16\"><path d=\"M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325\"/></svg>',\n 'plus': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus\" viewBox=\"0 0 16 16\"><path d=\"M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4\"/></svg>',\n 'plus-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus-lg\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2\"/></svg>',\n 'search': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-search\" viewBox=\"0 0 16 16\"><path d=\"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0\"/></svg>',\n 'trash': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-trash\" viewBox=\"0 0 16 16\"><path d=\"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z\"/><path d=\"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z\"/></svg>',\n 'x-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-lg\" viewBox=\"0 0 16 16\"><path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z\"/></svg>',\n};\n","import { Injectable, inject } from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { SPARK_BUILT_IN_ICONS } from './spark-built-in-icons';\n\n/**\n * R2-M12: SVG icon registry. The registry no longer accepts a raw string —\n * apps must pre-sanitize via `DomSanitizer.bypassSecurityTrustHtml` (or\n * `parseSvg` to validate) before calling `register`. Treating the parameter\n * as `SafeHtml` puts the explicit bypass decision in the caller's hands so a\n * developer wiring server-supplied SVG (per-tenant branding fetched from a\n * JSON endpoint) doesn't accidentally trust a `<svg><script>...</script>`\n * payload.\n *\n * The built-in icons baked into the package are still bypass-trusted internally\n * because they're known-safe at build time.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkIconRegistry {\n private sanitizer = inject(DomSanitizer);\n private icons = new Map<string, SafeHtml>();\n\n constructor() {\n for (const [name, svg] of Object.entries(SPARK_BUILT_IN_ICONS)) {\n this.icons.set(name, this.sanitizer.bypassSecurityTrustHtml(svg));\n }\n }\n\n /**\n * Registers a pre-sanitized SVG icon. Callers MUST validate or explicitly\n * trust the SVG before passing — for example:\n *\n * ```ts\n * const safe = sanitizer.bypassSecurityTrustHtml(svgFromBuildTimeConstant);\n * registry.register('app-logo', safe);\n * ```\n *\n * For server-supplied SVG, prefer a strict allow-list parser (no `<script>`,\n * no `on*` attributes, no `href=\"javascript:...\"`) before trusting.\n */\n register(name: string, svg: SafeHtml): void {\n this.icons.set(name, svg);\n }\n\n get(name: string): SafeHtml | undefined {\n return this.icons.get(name);\n }\n\n has(name: string): boolean {\n return this.icons.has(name);\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport { CustomActionDefinition, PersistentObject, SparkQuery, EntityType, filterQueryActions } from '@mintplayer/ng-spark/models';\nimport { SparkService } from './spark.service';\n\n/**\n * A query's custom actions, reachable **without the grid that usually renders them**.\n *\n * ## Why this exists\n *\n * A query's actions were only ever obtainable by rendering `<spark-query-grid>`, which loads the\n * query, resolves its entity type, fetches the actions and filters them to the ones marked for a\n * query surface — all privately. A page that wanted the same buttons somewhere else (a toolbar\n * above the card, the shell's topbar) had three bad options: duplicate the four-step resolution\n * and let it drift, reach into the grid's internals, or put the button in the wrong place.\n *\n * This exposes the resolution itself. The grid keeps rendering the default placement; a host that\n * wants a different one asks here and renders its own control.\n *\n * ## What it does not do\n *\n * It does not authorize. `/spark/actions/{type}` returns only the actions this caller may see —\n * the server filters against `security.json`, and executing re-checks — so nothing here is a gate,\n * and a host must not treat \"the list came back empty\" as anything other than a display fact.\n *\n * It also does not cache. Actions depend on the caller, and a memo keyed by type id would survive\n * a sign-out.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkQueryActionsService {\n private readonly sparkService = inject(SparkService);\n\n /**\n * The custom actions of the query named by id or alias, already filtered to those that belong on\n * a query surface.\n *\n * Returns an empty list — rather than throwing — when the query resolves to no entity type,\n * because a caller rendering a toolbar wants \"no buttons\", not a broken page.\n */\n async actionsFor(queryIdOrAlias: string): Promise<CustomActionDefinition[]> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) return [];\n\n // filterQueryActions, not a local predicate: the grid uses it too, and the `showedOn` values\n // are exactly the kind of thing a second copy gets subtly wrong — both grids once tested for\n // \"list\", a value nothing emits, and every correctly authored action rendered nowhere.\n return filterQueryActions(await this.sparkService.getCustomActions(context.entityType.id));\n }\n\n /**\n * Runs one of those actions. `selectedItemIds` are row ids, exactly as the grid posts them —\n * the server re-materializes each one through the same load path a detail page uses, so an id\n * from anywhere is treated as caller input rather than as a verified row.\n */\n async execute(\n queryIdOrAlias: string,\n actionName: string,\n options?: {\n parent?: PersistentObject;\n selectedItemIds?: string[];\n /** For a sub-query: the object whose detail page it was rendered on. See SparkService. */\n queryParent?: { id: string; type: string };\n },\n ): Promise<void> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) {\n throw new Error(\n `Cannot execute '${actionName}': query '${queryIdOrAlias}' resolves to no entity type, so there ` +\n `is nothing to execute it against.`);\n }\n\n await this.sparkService.executeCustomAction(\n context.entityType.id, actionName, options?.parent, options?.selectedItemIds, options?.queryParent,\n context.query.id);\n }\n\n /**\n * The query and the entity type its rows are mapped against — the two-step resolution both\n * methods need, kept in one place so they cannot disagree about which type an action runs on.\n */\n private async contextFor(\n queryIdOrAlias: string,\n ): Promise<{ query: SparkQuery; entityType: EntityType } | null> {\n const [query, entityTypes] = await Promise.all([\n this.sparkService.getQuery(queryIdOrAlias),\n this.sparkService.getEntityTypes(),\n ]);\n if (!query) return null;\n\n const entityType = entityTypes.find(t => t.name === query.entityType)\n ?? entityTypes.find(t => t.id === query.entityType);\n return entityType ? { query, entityType } : null;\n }\n}\n","import { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { HttpFeature, HttpFeatureKind, HttpInterceptorFn, withInterceptors } from '@angular/common/http';\nimport { PLATFORM_ID, inject } from '@angular/core';\n\n/**\n * The header carrying the viewer's IANA timezone id, e.g. `Europe/Brussels`.\n * Must stay in lockstep with `RequestTimeZoneResolver.HeaderName` on the server.\n */\nexport const SPARK_TIMEZONE_HEADER = 'X-Spark-Timezone';\n\n/**\n * The default name of the cookie carrying the same id, for requests the browser makes without any\n * script running first (a server-side render, a link opened from a mail). Must stay in lockstep\n * with `SparkTimeZoneOptions.DefaultCookieName` (`Spark:TimeZone:CookieName`) on the server.\n */\nexport const SPARK_TIMEZONE_COOKIE = 'spark-timezone';\n\n/** Options for {@link withSparkTimezone}. */\nexport interface SparkTimezoneOptions {\n /**\n * The cookie to write the viewer's zone into; `false` writes none (the header is still sent).\n * Defaults to {@link SPARK_TIMEZONE_COOKIE}. Use the same name as the server's\n * `Spark:TimeZone:CookieName`.\n */\n cookieName?: string | false;\n}\n\n/** One year: the zone only changes when the viewer travels, and each visit refreshes it. */\nconst COOKIE_MAX_AGE_SECONDS = 31_536_000;\n\n/** The server's shape check (`RequestTimeZoneResolver`): an id failing it would be ignored anyway. */\nconst ZONE_ID_SHAPE = /^[A-Za-z][A-Za-z0-9_+\\-]*(\\/[A-Za-z0-9_+\\-]+){0,2}$/;\n\n/**\n * Returns the browser's IANA zone id, or `null` when the environment cannot name one.\n *\n * An id is deliberately sent rather than a numeric offset: an offset is only valid at one instant,\n * so a server asked to render \"next Tuesday at 09:00\" from an offset captured today gets it wrong\n * across a DST boundary. A zone id carries the rules, not one sample of them.\n */\nexport function resolveBrowserTimeZone(): string | null {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || null;\n } catch {\n return null;\n }\n}\n\n/** The value of cookie `name` in `cookies` (`document.cookie`), or `null`. */\nfunction readCookie(cookies: string, name: string): string | null {\n for (const part of cookies.split(';')) {\n const eq = part.indexOf('=');\n if (eq > 0 && part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return null;\n}\n\n/**\n * Writes `zone` into cookie `name` unless it already holds it. Browser only; the server never\n * writes this cookie. `Secure` on https, `SameSite=Lax` so a link from a mail still carries it.\n */\nfunction writeTimezoneCookie(doc: Document, name: string, zone: string): void {\n if (readCookie(doc.cookie, name) === zone) return;\n const secure = doc.location?.protocol === 'https:' ? '; Secure' : '';\n doc.cookie = `${name}=${zone}; Path=/; Max-Age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax${secure}`;\n}\n\n/**\n * Builds the interceptor for {@link withSparkTimezone}. Exported for apps that compose their own\n * `withInterceptors([...])`.\n *\n * **This does not participate in reading or writing timestamps.** A `DateTimeOffset` means an\n * instant, and the browser converts in both directions on those paths. The header and cookie exist\n * for work the server does on its own — a scheduled export, a notification email, a server-side\n * render — where there is no browser in the loop to do the converting but the output still has to\n * read as local time.\n *\n * ⚠️ **Nothing happens on the server platform.** During a server-side render `Intl` names the Node\n * process's zone (UTC in a container), and a header carrying it would win over the viewer's cookie\n * on the server (spike S-TZ3, #460).\n */\nexport function createSparkTimezoneInterceptor(options: SparkTimezoneOptions = {}): HttpInterceptorFn {\n const cookieName = options.cookieName === undefined ? SPARK_TIMEZONE_COOKIE : options.cookieName;\n\n return (req, next) => {\n if (!isPlatformBrowser(inject(PLATFORM_ID))) return next(req);\n\n const zone = resolveBrowserTimeZone();\n if (!zone) return next(req);\n\n if (cookieName && ZONE_ID_SHAPE.test(zone)) {\n writeTimezoneCookie(inject(DOCUMENT), cookieName, zone);\n }\n\n // Never annotate a cross-origin call: the viewer's timezone is a (small) fingerprinting signal\n // and third-party hosts have no business receiving it.\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(req.url)) {\n try {\n if (new URL(req.url).origin !== window.location.origin) return next(req);\n } catch {\n return next(req);\n }\n }\n\n return next(req.clone({ setHeaders: { [SPARK_TIMEZONE_HEADER]: zone } }));\n };\n}\n\n/**\n * Attaches {@link SPARK_TIMEZONE_HEADER} to same-origin requests and keeps the\n * {@link SPARK_TIMEZONE_COOKIE} cookie current — with the default options. See\n * {@link createSparkTimezoneInterceptor}.\n */\nexport const sparkTimezoneInterceptor: HttpInterceptorFn = createSparkTimezoneInterceptor();\n\n/**\n * `provideHttpClient(...withSparkTimezone())` — registers the timezone interceptor.\n *\n * Spreadable alongside `withSparkAuth()`:\n * `provideHttpClient(...withSparkAuth(), ...withSparkTimezone())`.\n *\n * The cookie is written on the first request the browser app makes, so the very first server-side\n * render of a first visit has no cookie and renders in UTC.\n */\nexport function withSparkTimezone(options?: SparkTimezoneOptions): HttpFeature<HttpFeatureKind>[] {\n return [withInterceptors([options ? createSparkTimezoneInterceptor(options) : sparkTimezoneInterceptor])];\n}\n","import { inject, Injectable } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { SparkService } from './spark.service';\n\n/** The `history.state` key a list row link stores the list's own URL under. */\nexport const SPARK_RETURN_URL_STATE_KEY = 'sparkReturnUrl';\n\n/**\n * Where a detail page goes back to after its row stops existing — a Delete, or a Purge from the\n * recycle bin (#460).\n *\n * Neither \"the start page\" nor \"the previous history entry\" is the right answer: the first loses the\n * user's place, the second may be the deleted row's edit page, or another site. The right answer is\n * the list the row was opened from, so a list's row link records that list's URL in the navigation\n * state ({@link stateForRowLink}); a detail page reached otherwise (a bookmark, a reference link)\n * falls back to the row type's own list — its program-unit query, else any query over the type.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkReturnNavigationService {\n private readonly router = inject(Router);\n private readonly spark = inject(SparkService);\n\n /** Navigation state for a row link: the current URL (the list, with its `?deleted=` mode). */\n stateForRowLink(): Record<string, string> {\n return { [SPARK_RETURN_URL_STATE_KEY]: this.router.url };\n }\n\n /** The list URL the current detail page was opened from, when a row link recorded one. */\n recordedReturnUrl(): string | null {\n const state = (typeof history !== 'undefined' ? history.state : null) as Record<string, unknown> | null;\n const url = state?.[SPARK_RETURN_URL_STATE_KEY];\n // Only an in-app path: the state is the app's own, but a protocol-relative \"//host\" is not a path.\n return typeof url === 'string' && url.startsWith('/') && !url.startsWith('//') ? url : null;\n }\n\n /**\n * Navigates to the list the row came from, else to the type's list (`?deleted=only` kept when\n * `deletedView`), else to the start page. `replaceUrl`: the gone row's page is not worth a Back.\n */\n async returnToList(entityTypeName: string | null | undefined, options?: { deletedView?: boolean }): Promise<void> {\n const recorded = this.recordedReturnUrl();\n if (recorded) {\n await this.router.navigateByUrl(recorded, { replaceUrl: true });\n return;\n }\n const route = entityTypeName ? await this.listRouteFor(entityTypeName) : null;\n if (route) {\n await this.router.navigate(route, {\n replaceUrl: true,\n ...(options?.deletedView ? { queryParams: { deleted: 'only' } } : {}),\n });\n return;\n }\n await this.router.navigate(['/'], { replaceUrl: true });\n }\n\n /** `['/query', alias]` of the type's list: a menu (program-unit) query first, else any query. */\n async listRouteFor(entityTypeName: string): Promise<unknown[] | null> {\n let queries;\n try {\n queries = (await this.spark.getQueries()).filter(q => q.entityType === entityTypeName);\n } catch {\n return null;\n }\n if (!queries.length) return null;\n try {\n const units = (await this.spark.getProgramUnits()).programUnitGroups\n .flatMap(g => g.programUnits)\n .filter(u => u.type === 'query')\n .sort((a, b) => a.order - b.order);\n for (const unit of units) {\n if (queries.some(q => q.id === unit.queryId || (!!unit.alias && q.alias === unit.alias)))\n return ['/query', unit.alias || unit.queryId!];\n }\n } catch {\n // No menu: any query over the type still beats the start page.\n }\n return ['/query', queries[0].alias || queries[0].id];\n }\n}\n"],"mappings":"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"}
@@ -13,10 +13,11 @@ var SparkSoftDeleteService = class SparkSoftDeleteService {
13
13
  id
14
14
  });
15
15
  }
16
- async purge(type, id) {
16
+ async purge(type, id, etag) {
17
17
  await this.spark.postEnvelope("/po/purge", {
18
18
  objectTypeId: type,
19
- id
19
+ id,
20
+ etag
20
21
  });
21
22
  }
22
23
  static ɵfac = i0.ɵɵngDeclareFactory({
@@ -166,7 +167,7 @@ var SparkSoftDeleteActionsComponent = class SparkSoftDeleteActionsComponent {
166
167
  if (!confirm(this.lang.t("softDelete.confirmPurge"))) return;
167
168
  const ctx = this.context();
168
169
  await this.run(async () => {
169
- await this.softDelete.purge(ctx.type, ctx.id);
170
+ await this.softDelete.purge(ctx.type, ctx.id, ctx.item.etag ?? "");
170
171
  await this.returnNavigation.returnToList(ctx.entityType?.name, { deletedView: true });
171
172
  });
172
173
  }