@mintplayer/ng-spark 22.31.0 → 22.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +101 -1
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +3 -3
  3. package/fesm2022/mintplayer-ng-spark-auth-account.mjs +18 -18
  4. package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs +16 -8
  5. package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs +3 -3
  7. package/fesm2022/mintplayer-ng-spark-auth-core.mjs +6 -6
  8. package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs +3 -3
  9. package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs +3 -3
  10. package/fesm2022/mintplayer-ng-spark-auth-login.mjs +3 -3
  11. package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs +3 -3
  12. package/fesm2022/mintplayer-ng-spark-auth-register.mjs +3 -3
  13. package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs +3 -3
  14. package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs +3 -3
  15. package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs +3 -3
  16. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +24 -24
  17. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +3 -3
  18. package/fesm2022/mintplayer-ng-spark-contributions.mjs +9 -9
  19. package/fesm2022/mintplayer-ng-spark-grid.mjs +220 -49
  20. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  21. package/fesm2022/mintplayer-ng-spark-history.mjs +9 -9
  22. package/fesm2022/mintplayer-ng-spark-icon.mjs +3 -3
  23. package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs +3 -3
  24. package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs +9 -9
  25. package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs +6 -6
  26. package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs +3 -3
  27. package/fesm2022/mintplayer-ng-spark-models.mjs +44 -1
  28. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  29. package/fesm2022/mintplayer-ng-spark-moderation.mjs +21 -21
  30. package/fesm2022/mintplayer-ng-spark-notifications.mjs +687 -0
  31. package/fesm2022/mintplayer-ng-spark-notifications.mjs.map +1 -0
  32. package/fesm2022/mintplayer-ng-spark-organizations-accept.mjs +179 -0
  33. package/fesm2022/mintplayer-ng-spark-organizations-accept.mjs.map +1 -0
  34. package/fesm2022/mintplayer-ng-spark-organizations.mjs +202 -0
  35. package/fesm2022/mintplayer-ng-spark-organizations.mjs.map +1 -0
  36. package/fesm2022/mintplayer-ng-spark-pipes.mjs +125 -85
  37. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-po-create.mjs +3 -3
  39. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +90 -26
  40. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  41. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +84 -12
  42. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  43. package/fesm2022/mintplayer-ng-spark-po-form.mjs +9 -9
  44. package/fesm2022/mintplayer-ng-spark-pwa.mjs +74 -6
  45. package/fesm2022/mintplayer-ng-spark-pwa.mjs.map +1 -1
  46. package/fesm2022/mintplayer-ng-spark-query-list.mjs +10 -109
  47. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  48. package/fesm2022/mintplayer-ng-spark-renderers.mjs +3 -3
  49. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +3 -3
  50. package/fesm2022/mintplayer-ng-spark-score-renderers.mjs +801 -0
  51. package/fesm2022/mintplayer-ng-spark-score-renderers.mjs.map +1 -0
  52. package/fesm2022/mintplayer-ng-spark-services.mjs +424 -72
  53. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  54. package/fesm2022/mintplayer-ng-spark-share-page.mjs +334 -0
  55. package/fesm2022/mintplayer-ng-spark-share-page.mjs.map +1 -0
  56. package/fesm2022/mintplayer-ng-spark-share.mjs +10 -0
  57. package/fesm2022/mintplayer-ng-spark-share.mjs.map +1 -0
  58. package/fesm2022/mintplayer-ng-spark-shell.mjs +38 -36
  59. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  60. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +9 -9
  61. package/package.json +25 -1
  62. package/types/mintplayer-ng-spark-grid.d.ts +77 -26
  63. package/types/mintplayer-ng-spark-models.d.ts +150 -17
  64. package/types/mintplayer-ng-spark-notifications.d.ts +188 -0
  65. package/types/mintplayer-ng-spark-organizations-accept.d.ts +40 -0
  66. package/types/mintplayer-ng-spark-organizations.d.ts +72 -0
  67. package/types/mintplayer-ng-spark-pipes.d.ts +10 -0
  68. package/types/mintplayer-ng-spark-po-detail.d.ts +26 -0
  69. package/types/mintplayer-ng-spark-po-edit.d.ts +37 -0
  70. package/types/mintplayer-ng-spark-pwa.d.ts +48 -2
  71. package/types/mintplayer-ng-spark-query-list.d.ts +8 -23
  72. package/types/mintplayer-ng-spark-score-renderers.d.ts +340 -0
  73. package/types/mintplayer-ng-spark-services.d.ts +146 -6
  74. package/types/mintplayer-ng-spark-share-page.d.ts +67 -0
  75. package/types/mintplayer-ng-spark-share.d.ts +26 -0
@@ -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","../../services/src/spark-datatable-labels.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, PLATFORM_ID, inject, signal } from '@angular/core';\nimport { DOCUMENT, isPlatformBrowser } from '@angular/common';\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\n/** One year, in seconds: the `spark-lang` cookie outlives sessions like the localStorage entry it mirrors. */\nconst LANGUAGE_COOKIE_MAX_AGE = 365 * 24 * 60 * 60;\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 /** Null outside a browser (server-side rendering), where there is no cookie to write. */\n private readonly document = isPlatformBrowser(inject(PLATFORM_ID)) ? inject(DOCUMENT) : null;\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 this.writeCookie(lang);\n }\n\n /**\n * Mirrors the language into the `spark-lang` cookie, which the server-rendered pages read\n * (`/connect/*` of the identity provider, `ConnectText.CultureCookie`): they cannot see\n * localStorage, and without the cookie a sign-in popup would open in the browser's language\n * rather than the one the user picked in the app. Not `HttpOnly` by nature (script writes it);\n * it carries only a language code, which the server matches against its supported cultures.\n */\n private writeCookie(lang: string): void {\n const document = this.document;\n if (!document || !lang) return;\n const secure = document.location?.protocol === 'https:' ? '; Secure' : '';\n document.cookie = `spark-lang=${encodeURIComponent(lang)}; Path=/; Max-Age=${LANGUAGE_COOKIE_MAX_AGE}; SameSite=Lax${secure}`;\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 this.writeCookie(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, SPARK_RETRY_CANCEL } from '@mintplayer/ng-spark/models';\nimport { ClientOperationEnvelope, RetryOperation, SparkClientMethodRegistry, 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 private readonly clientMethods = inject(SparkClientMethodRegistry);\n\n // Entity Types\n async getEntityTypes(): Promise<EntityType[]> {\n return firstValueFrom(this.http.get<EntityType[]>(`${this.baseUrl}/types`));\n }\n\n /**\n * One entity type, shaped by the caller's attribute rights. `purpose` narrows it for one form (#264, G1/G2):\n * `'new'` leaves out what the caller may not create and does not apply edit-only denies.\n */\n async getEntityType(id: string, purpose?: 'new' | 'edit' | 'read'): Promise<EntityType> {\n const query = purpose ? `?for=${purpose}` : '';\n return firstValueFrom(this.http.get<EntityType>(`${this.baseUrl}/types/${encodeURIComponent(id)}${query}`));\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 // A client-method step (`IRetryAccessor.Invoke`): no modal, the registered method answers. Any\n // failure, an unknown name included, is sent to the server as Cancel rather than thrown, because\n // the server action decides what a cancelled browser step means (PRD D7, Q9).\n if (retryOp.clientMethod) {\n const outcome = await this.clientMethods.invoke(retryOp.clientMethod, retryOp.arguments);\n const answer: RetryActionResult = outcome.ok\n ? { step: retryOp.step, option: 'OK', value: outcome.value }\n : { step: retryOp.step, option: SPARK_RETRY_CANCEL };\n body.retryResults = [...(body.retryResults || []), answer];\n return retryFn();\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 cancellable: retryOp.cancellable === true,\n };\n const result = await this.retryActionService.show(payload);\n // A dismissal of a prompt that did not ask for a Cancel abandons the request: the server never\n // offered one, so it is not told of one. Whether it did is the flag, never an option's label.\n if (result.option === SPARK_RETRY_CANCEL && !payload.cancellable) 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","import { SparkLanguageService } from './spark-language.service';\n\n/** The `<bs-datatable>` strings Spark translates: the column-resize handle and its options dialog. */\nexport interface SparkDatatableLabels {\n resizeColumn: (column: string) => string;\n resizeColumnHint: string;\n resizeColumnOptions: (column: string) => string;\n narrowerColumn: (column: string) => string;\n widerColumn: (column: string) => string;\n columnWidth: (px: number) => string;\n fitColumn: string;\n resetColumn: string;\n}\n\n/**\n * The `[labels]` a Spark `<bs-datatable>` passes so the column-resize handle, its keymap\n * announcement and its resize options dialog (ng-bootstrap 22.22.0 / web-components 2.19.0) speak\n * the viewer's language.\n *\n * The `column` these formatters receive is the column's label: every Spark grid binds\n * `bsDatatableColumnLabel` to the header's translated text (and the row-actions column to\n * \"Actions\"), so the datatable no longer falls back to the internal name. The keys omitted here\n * keep the web component's English defaults.\n *\n * Every translation is read EAGERLY, so a `computed` that calls this tracks the language and the\n * translation map, and hands the datatable a new object — which re-renders it — when either changes.\n */\nexport function sparkDatatableLabels(lang: SparkLanguageService): SparkDatatableLabels {\n const column = (key: string) => {\n const template = lang.t(key);\n return (name: string) => template.replace('{column}', name);\n };\n const columnWidth = lang.t('common.columnWidth');\n return {\n resizeColumn: column('common.resizeColumn'),\n resizeColumnHint: lang.t('common.resizeColumnHint'),\n resizeColumnOptions: column('common.resizeColumnOptions'),\n narrowerColumn: column('common.narrowerColumn'),\n widerColumn: column('common.widerColumn'),\n columnWidth: (px: number) => columnWidth.replace('{px}', String(px)),\n fitColumn: lang.t('common.fitColumn'),\n resetColumn: lang.t('common.resetColumn'),\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-live.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","../../services/src/spark-datatable-labels.ts","../../services/src/spark-error.ts","../../services/src/spark-language.interceptor.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, PLATFORM_ID, inject, signal } from '@angular/core';\nimport { DOCUMENT, isPlatformBrowser } from '@angular/common';\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\n/** One year, in seconds: the `spark-lang` cookie outlives sessions like the localStorage entry it mirrors. */\nconst LANGUAGE_COOKIE_MAX_AGE = 365 * 24 * 60 * 60;\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 /** Null outside a browser (server-side rendering), where there is no cookie to write. */\n private readonly document = isPlatformBrowser(inject(PLATFORM_ID)) ? inject(DOCUMENT) : null;\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 this.writeCookie(lang);\n }\n\n /**\n * Mirrors the language into the `spark-lang` cookie, which the server-rendered pages read\n * (`/connect/*` of the identity provider, `ConnectText.CultureCookie`): they cannot see\n * localStorage, and without the cookie a sign-in popup would open in the browser's language\n * rather than the one the user picked in the app. Not `HttpOnly` by nature (script writes it);\n * it carries only a language code, which the server matches against its supported cultures.\n */\n private writeCookie(lang: string): void {\n const document = this.document;\n if (!document || !lang) return;\n const secure = document.location?.protocol === 'https:' ? '; Secure' : '';\n document.cookie = `spark-lang=${encodeURIComponent(lang)}; Path=/; Max-Age=${LANGUAGE_COOKIE_MAX_AGE}; SameSite=Lax${secure}`;\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 this.writeCookie(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 /**\n * The text of `key` in the current language. When there is none (unknown key, empty text, or the\n * translations have not arrived yet) it is `fallback` when given, else the key itself, so\n * `t('spark.query.unavailable', 'This list is not available.')` never shows a raw key to a user.\n */\n t(key: string, fallback?: string): string {\n const ts = this.translationsMap()[key];\n return this.resolve(ts) || fallback || key;\n }\n}\n","import { DestroyRef, Injectable, Injector, NgZone, Signal, assertInInjectionContext, effect, inject, signal, untracked } from '@angular/core';\nimport { BehaviorSubject, Observable, Subscription, isObservable, of } from 'rxjs';\nimport {\n LIVE_CLOSE_MESSAGE_TOO_LARGE,\n LIVE_CLOSE_REAUTHENTICATE,\n LIVE_CLOSE_TOO_MANY_SOCKETS,\n LiveClientOp,\n LiveErrorEvent,\n LiveFailureCode,\n LivePoEvent,\n LivePoRequest,\n LiveQueryEvent,\n LiveQueryRequest,\n LiveServerFrame,\n PersistentObject,\n QueryResult,\n SparkDeletedFilter,\n applyLivePoPatch,\n applyLiveQueryPatch,\n} from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n/** What a watcher of one subscription is told. */\ninterface Watcher {\n next(event: LivePoEvent | LiveQueryEvent): void;\n complete(): void;\n}\n\n/** One server subscription, shared by every watcher of the same canonical request. */\ninterface Entry {\n key: string;\n sub: string;\n kind: 'po' | 'query';\n request: LivePoRequest | LiveQueryRequest;\n watchers: Set<Watcher>;\n /** The state with every frame applied; null until the first `po`/`result` (and again after a reconnect). */\n state: PersistentObject | QueryResult | null;\n}\n\n/** Options of {@link SparkLiveService.watchPersistentObject}. */\nexport interface LivePoWatchOptions {\n /** The soft-deletion mode the object is loaded with, as `/spark/po/load` takes it. */\n deleted?: SparkDeletedFilter | null;\n}\n\n/** Options of the signal helpers: an injector when called outside an injection context. */\nexport interface LiveSignalOptions {\n injector?: Injector;\n}\n\n/** The first reconnect delay, doubled per attempt up to {@link MAX_DELAY_MS}, then jittered to 50–100 %. */\nconst BASE_DELAY_MS = 1000;\nconst MAX_DELAY_MS = 30000;\n/** A second 4401 this soon after reconnecting for one is not a lifetime rotation: the login is gone. */\nconst REAUTH_LOOP_MS = 5000;\nconst OPEN = 1;\n\n/**\n * Live updates over ONE lazy WebSocket to `/spark/live` (X4; `docs/guide-live-updates.md`).\n *\n * - Subscriptions are ref-counted by a canonical JSON of their request, so two components watching the\n * same object or the same query page share one server subscription; the last one to leave unsubscribes,\n * and the socket closes when nothing is watched.\n * - `po`/`result` replace the held state and `poPatch`/`patch` are applied to it, so a watcher always\n * receives the whole object or page.\n * - A subscription's `error` or `gone` ends that subscription only: it is never a reason to reconnect.\n * (The streaming service it replaces retried ten times on \"access denied\".)\n * - The socket reconnects with exponential backoff and jitter after a network loss or a server restart,\n * then resubscribes everything, which yields fresh snapshots. `4401` reconnects at once (the cookie is\n * validated afresh) unless it repeats within seconds; `4429` and `1009` are surfaced, not retried.\n * - The socket and its timers run outside the Angular zone; only emissions re-enter it.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkLiveService {\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 private readonly entries = new Map<string, Entry>();\n private readonly bySub = new Map<string, Entry>();\n private nextSub = 0;\n\n private socket: WebSocket | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private attempt = 0;\n private lastReauthAt = 0;\n\n /**\n * Follows one persistent object: `po` with the object as `/spark/po/load` returns it (patches applied),\n * then `gone` or `error`, after either of which the stream completes.\n */\n watchPersistentObject(objectTypeId: string, id: string, options?: LivePoWatchOptions): Observable<LivePoEvent> {\n const request: LivePoRequest = { objectTypeId, id, deleted: options?.deleted ?? null };\n return new Observable<LivePoEvent>(subscriber => {\n const watcher: Watcher = {\n next: event => subscriber.next(event as LivePoEvent),\n complete: () => subscriber.complete(),\n };\n const entry = this.attach('po', request, watcher);\n return () => this.detach(entry, watcher);\n });\n }\n\n /**\n * Follows one query page: `result` with the page as `/spark/queries/execute` returns it (patches applied),\n * or `error`, after which the stream completes.\n *\n * Given an observable, the page follows it: a new request is sent as an `update` of the same server\n * subscription when this watcher has it to itself, and joins (or opens) the subscription of the new request\n * otherwise. `null` leaves the current subscription without completing.\n */\n watchQuery(request: LiveQueryRequest | Observable<LiveQueryRequest | null>): Observable<LiveQueryEvent> {\n const requests = isObservable(request) ? request : of(request);\n return new Observable<LiveQueryEvent>(subscriber => {\n let entry: Entry | null = null;\n let ended = false;\n const watcher: Watcher = {\n next: event => subscriber.next(event as LiveQueryEvent),\n complete: () => {\n ended = true;\n entry = null;\n subscriber.complete();\n },\n };\n\n const requestSubscription: Subscription = requests.subscribe(next => {\n if (ended) return;\n if (!next) {\n if (entry) this.detach(entry, watcher);\n entry = null;\n return;\n }\n const key = keyOf('query', next);\n if (entry?.key === key) return;\n if (entry && entry.watchers.size === 1 && !this.entries.has(key)) {\n this.update(entry, key, next);\n return;\n }\n if (entry) this.detach(entry, watcher);\n entry = this.attach('query', next, watcher);\n });\n\n return () => {\n requestSubscription.unsubscribe();\n if (entry) this.detach(entry, watcher);\n entry = null;\n };\n });\n }\n\n /**\n * A signal of one live object for a custom page: `null` until the first state (and after an `error`),\n * the object afterwards, `'gone'` once it is deleted or no longer readable. Follows `type` and `id`;\n * a null or empty id watches nothing. Call it in an injection context or pass an injector.\n */\n livePersistentObject(type: Signal<string>, id: Signal<string | null | undefined>, options?: LiveSignalOptions): Signal<PersistentObject | null | 'gone'> {\n if (!options?.injector) assertInInjectionContext(this.livePersistentObject);\n const injector = options?.injector ?? inject(Injector);\n const value = signal<PersistentObject | null | 'gone'>(null);\n effect(onCleanup => {\n const objectTypeId = type();\n const objectId = id();\n untracked(() => value.set(null));\n if (!objectTypeId || !objectId) return;\n const subscription = this.watchPersistentObject(objectTypeId, objectId).subscribe(event => {\n if (event.type === 'po') value.set(event.po);\n else if (event.type === 'gone') value.set('gone');\n else value.set(null);\n });\n onCleanup(() => subscription.unsubscribe());\n }, { injector });\n return value.asReadonly();\n }\n\n /**\n * A signal of one live query page for a custom page: `null` until the first page (and after an `error`).\n * A changed request (page, sort, search) is sent as an `update`; `null` watches nothing. Call it in an\n * injection context or pass an injector.\n */\n liveQuery(request: Signal<LiveQueryRequest | null>, options?: LiveSignalOptions): Signal<QueryResult | null> {\n if (!options?.injector) assertInInjectionContext(this.liveQuery);\n const injector = options?.injector ?? inject(Injector);\n const value = signal<QueryResult | null>(null);\n const requests = new BehaviorSubject<LiveQueryRequest | null>(null);\n const subscription = this.watchQuery(requests).subscribe(event => {\n value.set(event.type === 'result' ? event.result : null);\n });\n effect(() => {\n const next = request();\n untracked(() => requests.next(next));\n }, { injector });\n injector.get(DestroyRef).onDestroy(() => {\n subscription.unsubscribe();\n requests.complete();\n });\n return value.asReadonly();\n }\n\n // --- subscriptions -------------------------------------------------------------------------------\n\n private attach(kind: 'po' | 'query', request: LivePoRequest | LiveQueryRequest, watcher: Watcher): Entry {\n const key = keyOf(kind, request);\n let entry = this.entries.get(key);\n if (entry) {\n entry.watchers.add(watcher);\n // A late joiner starts from the state the others already hold.\n if (entry.state) watcher.next(kind === 'po'\n ? { type: 'po', po: entry.state as PersistentObject }\n : { type: 'result', result: entry.state as QueryResult });\n return entry;\n }\n\n entry = { key, sub: `s${++this.nextSub}`, kind, request: clean(request), watchers: new Set([watcher]), state: null };\n this.entries.set(key, entry);\n this.bySub.set(entry.sub, entry);\n if (this.isOpen()) this.sendSubscribe(entry);\n else this.ensureSocket();\n return entry;\n }\n\n private update(entry: Entry, key: string, request: LiveQueryRequest): void {\n this.entries.delete(entry.key);\n entry.key = key;\n entry.request = clean(request);\n this.entries.set(key, entry);\n // While the socket is down the resubscribe on open carries the new request.\n if (this.isOpen()) this.send({ op: 'update', sub: entry.sub, query: entry.request as LiveQueryRequest });\n }\n\n private detach(entry: Entry, watcher: Watcher): void {\n if (!entry.watchers.delete(watcher)) return;\n if (entry.watchers.size > 0 || this.entries.get(entry.key) !== entry) return;\n this.forget(entry);\n if (this.isOpen()) this.send({ op: 'unsubscribe', sub: entry.sub });\n this.closeIfIdle();\n }\n\n private forget(entry: Entry): void {\n if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);\n this.bySub.delete(entry.sub);\n }\n\n /** Ends one subscription for all its watchers: the event, then completion. */\n private end(entry: Entry, event: LivePoEvent | LiveQueryEvent): void {\n this.forget(entry);\n const watchers = [...entry.watchers];\n entry.watchers.clear();\n this.emit(() => {\n for (const watcher of watchers) {\n watcher.next(event);\n watcher.complete();\n }\n });\n }\n\n /** Ends every subscription with a socket-level failure and drops the socket without reconnecting. */\n private failAll(code: LiveFailureCode, message: string): void {\n const event: LiveErrorEvent = { type: 'error', code, message };\n for (const entry of [...this.entries.values()]) this.end(entry, event);\n this.closeSocket();\n }\n\n // --- the socket ----------------------------------------------------------------------------------\n\n private isOpen(): boolean {\n return this.socket !== null && this.socket.readyState === OPEN;\n }\n\n private ensureSocket(): void {\n if (this.socket || this.reconnectTimer) return;\n this.open();\n }\n\n private open(): void {\n this.ngZone.runOutsideAngular(() => {\n const socket = new WebSocket(this.buildWebSocketUrl());\n this.socket = socket;\n socket.onopen = () => {\n if (this.socket !== socket) return;\n for (const entry of this.entries.values()) this.sendSubscribe(entry);\n };\n socket.onmessage = event => {\n if (this.socket !== socket) return;\n this.onFrame(event.data);\n };\n socket.onerror = () => {\n // onclose follows and decides.\n };\n socket.onclose = event => {\n if (this.socket !== socket) return;\n this.socket = null;\n this.onClose(event.code);\n };\n });\n }\n\n private onClose(code: number): void {\n if (this.entries.size === 0) return;\n // Every subscription starts over on the next socket with a fresh snapshot.\n for (const entry of this.entries.values()) entry.state = null;\n\n if (code === LIVE_CLOSE_TOO_MANY_SOCKETS) {\n this.failAll('tooManySockets', 'Too many live connections are open for this user.');\n return;\n }\n if (code === LIVE_CLOSE_MESSAGE_TOO_LARGE) {\n this.failAll('messageTooLarge', 'A live request was too large.');\n return;\n }\n if (code === LIVE_CLOSE_REAUTHENTICATE) {\n const now = Date.now();\n if (this.lastReauthAt && now - this.lastReauthAt < REAUTH_LOOP_MS) {\n this.lastReauthAt = 0;\n this.failAll('unauthenticated', 'The live connection was refused: sign in again.');\n return;\n }\n this.lastReauthAt = now;\n this.open();\n return;\n }\n\n const delay = Math.min(BASE_DELAY_MS * Math.pow(2, this.attempt), MAX_DELAY_MS) * (0.5 + Math.random() / 2);\n this.attempt++;\n this.ngZone.runOutsideAngular(() => {\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n if (this.entries.size > 0) this.open();\n }, delay);\n });\n }\n\n /**\n * Closes the socket once nothing is watched — after a microtask, so a watcher that switches objects\n * (leave one, watch the next in the same turn) keeps the socket instead of reconnecting.\n */\n private closeIfIdle(): void {\n if (this.entries.size > 0) return;\n void Promise.resolve().then(() => {\n if (this.entries.size === 0) this.closeSocket();\n });\n }\n\n private closeSocket(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n const socket = this.socket;\n this.socket = null;\n this.attempt = 0;\n if (socket && socket.readyState <= OPEN) socket.close(1000, 'No live subscriptions');\n }\n\n private sendSubscribe(entry: Entry): void {\n this.send(entry.kind === 'po'\n ? { op: 'subscribe', sub: entry.sub, po: entry.request as LivePoRequest }\n : { op: 'subscribe', sub: entry.sub, query: entry.request as LiveQueryRequest });\n }\n\n private send(op: LiveClientOp): void {\n this.socket?.send(JSON.stringify(op));\n }\n\n private onFrame(data: unknown): void {\n let frame: LiveServerFrame;\n try {\n frame = JSON.parse(String(data));\n } catch {\n return;\n }\n if (!frame || typeof frame !== 'object') return;\n // The connection is healthy again: the next loss starts the backoff from its first step.\n this.attempt = 0;\n if (frame.type === 'ping') return;\n if (frame.type === 'error' && !frame.sub) {\n console.warn(`[spark] live: ${frame.code}: ${frame.message}`);\n return;\n }\n\n const entry = this.bySub.get((frame as { sub: string }).sub);\n if (!entry) return;\n\n switch (frame.type) {\n case 'po':\n entry.state = frame.po;\n this.broadcast(entry, { type: 'po', po: frame.po });\n break;\n case 'poPatch':\n if (!entry.state) return;\n entry.state = applyLivePoPatch(entry.state as PersistentObject, frame.patch);\n this.broadcast(entry, { type: 'po', po: entry.state as PersistentObject });\n break;\n case 'gone':\n this.end(entry, { type: 'gone' });\n this.closeIfIdle();\n break;\n case 'result':\n entry.state = frame.result;\n this.broadcast(entry, { type: 'result', result: frame.result });\n break;\n case 'patch':\n if (!entry.state) return;\n entry.state = applyLiveQueryPatch(entry.state as QueryResult, frame.patch);\n this.broadcast(entry, { type: 'result', result: entry.state as QueryResult });\n break;\n case 'error':\n this.end(entry, { type: 'error', code: frame.code, message: frame.message });\n this.closeIfIdle();\n break;\n }\n }\n\n private broadcast(entry: Entry, event: LivePoEvent | LiveQueryEvent): void {\n const watchers = [...entry.watchers];\n this.emit(() => { for (const watcher of watchers) watcher.next(event); });\n }\n\n private emit(fn: () => void): void {\n this.ngZone.run(fn);\n }\n\n private buildWebSocketUrl(): string {\n const path = `${this.baseUrl}/live`;\n if (this.baseUrl.startsWith('http://')) return path.replace(/^http:\\/\\//, 'ws://');\n if (this.baseUrl.startsWith('https://')) return path.replace(/^https:\\/\\//, 'wss://');\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${window.location.host}${path}`;\n }\n}\n\n/** The request without `null`/`undefined` fields — what is sent, and what is keyed. */\nfunction clean<T extends object>(request: T): T {\n return JSON.parse(canonicalJson(request)) as T;\n}\n\nfunction keyOf(kind: 'po' | 'query', request: object): string {\n return `${kind}:${canonicalJson(request)}`;\n}\n\n/** JSON with object keys sorted and `null`/`undefined` fields dropped, so equal requests key equal. */\nexport function canonicalJson(value: unknown): string {\n return JSON.stringify(normalize(value));\n}\n\nfunction normalize(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(normalize);\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value).sort()) {\n const field = (value as Record<string, unknown>)[key];\n if (field === null || field === undefined) continue;\n result[key] = normalize(field);\n }\n return result;\n }\n return value;\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, SPARK_RETRY_CANCEL } from '@mintplayer/ng-spark/models';\nimport { ClientOperationEnvelope, RetryOperation, SparkClientMethodRegistry, 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 private readonly clientMethods = inject(SparkClientMethodRegistry);\n\n // Entity Types\n async getEntityTypes(): Promise<EntityType[]> {\n return firstValueFrom(this.http.get<EntityType[]>(`${this.baseUrl}/types`));\n }\n\n /**\n * One entity type, shaped by the caller's attribute rights. `purpose` narrows it for one form (#264, G1/G2):\n * `'new'` leaves out what the caller may not create and does not apply edit-only denies.\n */\n async getEntityType(id: string, purpose?: 'new' | 'edit' | 'read'): Promise<EntityType> {\n const query = purpose ? `?for=${purpose}` : '';\n return firstValueFrom(this.http.get<EntityType>(`${this.baseUrl}/types/${encodeURIComponent(id)}${query}`));\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 // A client-method step (`IRetryAccessor.Invoke`): no modal, the registered method answers. Any\n // failure, an unknown name included, is sent to the server as Cancel rather than thrown, because\n // the server action decides what a cancelled browser step means (PRD D7, Q9).\n if (retryOp.clientMethod) {\n const outcome = await this.clientMethods.invoke(retryOp.clientMethod, retryOp.arguments);\n const answer: RetryActionResult = outcome.ok\n ? { step: retryOp.step, option: 'OK', value: outcome.value }\n : { step: retryOp.step, option: SPARK_RETRY_CANCEL };\n body.retryResults = [...(body.retryResults || []), answer];\n return retryFn();\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 cancellable: retryOp.cancellable === true,\n };\n const result = await this.retryActionService.show(payload);\n // A dismissal of a prompt that did not ask for a Cancel abandons the request: the server never\n // offered one, so it is not told of one. Whether it did is the flag, never an option's label.\n if (result.option === SPARK_RETRY_CANCEL && !payload.cancellable) 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 'bell': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-bell\" viewBox=\"0 0 16 16\"><path d=\"M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2M8 1.918l-.797.161A4 4 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4 4 0 0 0-3.203-3.92zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5 5 0 0 1 13 6c0 .88.32 4.2 1.22 6\"/></svg>',\n 'share': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-share\" viewBox=\"0 0 16 16\"><path d=\"M13.5 1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3M11 2.5a2.5 2.5 0 1 1 .603 1.628l-6.718 3.12a2.5 2.5 0 0 1 0 1.504l6.718 3.12a2.5 2.5 0 1 1-.488.876l-6.718-3.12a2.5 2.5 0 1 1 0-3.256l6.718-3.12A2.5 2.5 0 0 1 11 2.5m-8.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3m11 5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3\"/></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 'person': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-person\" viewBox=\"0 0 16 16\"><path d=\"M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z\"/></svg>',\n 'box-arrow-right': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-box-arrow-right\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0z\"/><path fill-rule=\"evenodd\" d=\"M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708z\"/></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","import { SparkLanguageService } from './spark-language.service';\n\n/** The `<bs-datatable>` strings Spark translates: the column-resize handle and its options dialog. */\nexport interface SparkDatatableLabels {\n resizeColumn: (column: string) => string;\n resizeColumnHint: string;\n resizeColumnOptions: (column: string) => string;\n narrowerColumn: (column: string) => string;\n widerColumn: (column: string) => string;\n columnWidth: (px: number) => string;\n fitColumn: string;\n resetColumn: string;\n}\n\n/**\n * The `[labels]` a Spark `<bs-datatable>` passes so the column-resize handle, its keymap\n * announcement and its resize options dialog (ng-bootstrap 22.22.0 / web-components 2.19.0) speak\n * the viewer's language.\n *\n * The `column` these formatters receive is the column's label: every Spark grid binds\n * `bsDatatableColumnLabel` to the header's translated text (and the row-actions column to\n * \"Actions\"), so the datatable no longer falls back to the internal name. The keys omitted here\n * keep the web component's English defaults.\n *\n * Every translation is read EAGERLY, so a `computed` that calls this tracks the language and the\n * translation map, and hands the datatable a new object — which re-renders it — when either changes.\n */\nexport function sparkDatatableLabels(lang: SparkLanguageService): SparkDatatableLabels {\n const column = (key: string) => {\n const template = lang.t(key);\n return (name: string) => template.replace('{column}', name);\n };\n const columnWidth = lang.t('common.columnWidth');\n return {\n resizeColumn: column('common.resizeColumn'),\n resizeColumnHint: lang.t('common.resizeColumnHint'),\n resizeColumnOptions: column('common.resizeColumnOptions'),\n narrowerColumn: column('common.narrowerColumn'),\n widerColumn: column('common.widerColumn'),\n columnWidth: (px: number) => columnWidth.replace('{px}', String(px)),\n fitColumn: lang.t('common.fitColumn'),\n resetColumn: lang.t('common.resetColumn'),\n };\n}\n","import { HttpErrorResponse } from '@angular/common/http';\nimport type { TranslatedString } from '@mintplayer/ng-spark/models';\n\n/** What {@link describeSparkError} needs of the language service (`SparkLanguageService` fits). */\nexport interface SparkErrorLanguage {\n t(key: string, fallback?: string): string;\n resolve(ts: TranslatedString | undefined): string;\n}\n\ntype ErrorBody = {\n error?: unknown;\n retryAfterSeconds?: unknown;\n result?: { error?: unknown; retryAfterSeconds?: unknown; errors?: { errorMessage?: unknown }[] } | null;\n} | string | null | undefined;\n\n/**\n * The seconds a refused call (429) was told to wait: the envelope's `result.retryAfterSeconds` (core's\n * `ClientResult.Throttled`), a bare body's `retryAfterSeconds`, else the `Retry-After` header in seconds. `null` when\n * the refusal named no wait, or the error is not a 429.\n */\nexport function sparkRetryAfterSeconds(e: unknown): number | null {\n const err = e as HttpErrorResponse | null | undefined;\n if (err?.status !== 429) return null;\n const body = err.error as ErrorBody;\n const fromBody = typeof body === 'object' && body ? (body.result?.retryAfterSeconds ?? body.retryAfterSeconds) : undefined;\n if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody >= 0) return Math.ceil(fromBody);\n const header = err.headers?.get?.('Retry-After');\n if (header && /^\\d+$/.test(header.trim())) return Number(header.trim());\n return null;\n}\n\n/**\n * A wait in words, rounded the way a person says it: \"a few seconds\", \"1 minute\", \"12 minutes\", \"3 hours\", \"2 days\"\n * (keys `spark.wait.*`, `{0}` is the number).\n */\nexport function formatSparkWait(seconds: number, lang: Pick<SparkErrorLanguage, 't'>): string {\n const s = Math.max(0, Math.ceil(seconds));\n const say = (unit: string, n: number) =>\n lang.t(`spark.wait.${unit}${n === 1 ? '' : 's'}`, n === 1 ? `1 ${unit}` : `{0} ${unit}s`).replace('{0}', String(n));\n if (s < 60) return lang.t('spark.wait.fewSeconds', 'a few seconds');\n if (s < 3600) return say('minute', Math.ceil(s / 60));\n if (s < 2 * 86400) return say('hour', Math.max(1, Math.round(s / 3600)));\n return say('day', Math.round(s / 86400));\n}\n\n/**\n * The readable reason a Spark call failed, for an error banner: the first validation message (a `400`'s\n * `result.errors`), else the server's `result.error` (the envelope every action and write answers with), else a bare\n * body's `error`, else `fallback`. A refusal with a wait (429 + `retryAfterSeconds`/`Retry-After`) gets \"Try again in\n * 3 hours.\" appended, so a quota or cool-down says when, not just no.\n */\nexport function describeSparkError(e: unknown, lang: SparkErrorLanguage, fallback: string): string {\n const err = e as HttpErrorResponse | null | undefined;\n const body = err?.error as ErrorBody;\n let message: string | null = null;\n if (typeof body === 'object' && body) {\n const first = body.result?.errors?.[0]?.errorMessage;\n if (typeof first === 'string' && first) message = first;\n else if (first && typeof first === 'object') message = lang.resolve(first as TranslatedString) || null;\n if (!message && typeof body.result?.error === 'string' && body.result.error) message = body.result.error;\n if (!message && typeof body.error === 'string' && body.error) message = body.error;\n }\n message ??= fallback;\n\n const wait = sparkRetryAfterSeconds(e);\n if (wait === null) return message;\n const tryAgain = lang.t('spark.tryAgainIn', 'Try again in {0}.').replace('{0}', formatSparkWait(wait, lang));\n return `${message.trim()} ${tryAgain}`;\n}\n","import { HttpFeature, HttpFeatureKind, HttpInterceptorFn, withInterceptors } from '@angular/common/http';\nimport { currentLanguage } from '@mintplayer/ng-spark/models';\n\n/**\n * Sends the language the user picked to the server, as `Accept-Language`.\n *\n * `SparkLanguageService` keeps the choice in `localStorage` and never puts it on the wire: it is a\n * client-side concern, because almost everything translated reaches the client as a whole\n * `TranslatedString` and is resolved by the `resolveTranslation` pipe. Server-*resolved* strings\n * are the exception (CodeCoverage's Home page title, SeoTester's refusals and its dashboard title) because a `PersistentObject`'s breadcrumb and an\n * attribute's value are plain strings with nowhere to carry three languages.\n *\n * Spark resolves those through `IRequestCultureResolver`, which reads `Accept-Language`. Without\n * this the browser's header wins, so a visitor whose browser prefers Dutch sees a Dutch title\n * above an otherwise English page, and switching the language selector never changes it.\n *\n * ⚠️ Reads the module-level `currentLanguage` signal, and **must not** `inject(SparkLanguageService)`.\n * That service fetches `/culture` and `/translations` from its own constructor, so an interceptor\n * that injects it is re-entered while it is still being constructed: Angular throws NG0200 for\n * every request, the culture and translation payloads never arrive, and the whole UI renders raw\n * keys (\"app.title\") with the language selector missing. `currentLanguage` is the same value\n * without the cycle — the service writes it on load and on every change.\n *\n * Empty until `/culture` resolves, which is exactly the window where there is no user choice to\n * honour yet; those first requests fall through to the browser's own header.\n */\nexport const sparkLanguageInterceptor: HttpInterceptorFn = (req, next) => {\n const language = currentLanguage();\n if (!language) return next(req);\n return next(req.clone({ setHeaders: { 'Accept-Language': language } }));\n};\n\n/**\n * `provideHttpClient(...withSparkLanguage())`: registers {@link sparkLanguageInterceptor}, so server-resolved texts\n * (breadcrumbs, action messages, refusals) follow the language selector instead of the browser's preference.\n * Moved from CodeCoverage (M11), where it was the app's own; SeoTester needed the same.\n */\nexport function withSparkLanguage(): HttpFeature<HttpFeatureKind>[] {\n return [withInterceptors([sparkLanguageInterceptor])];\n}\n"],"mappings":"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"}