@mintplayer/ng-spark 22.30.0 → 22.32.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 (84) hide show
  1. package/README.md +2 -2
  2. package/auth/README.md +216 -0
  3. package/fesm2022/mintplayer-ng-spark-auth-account.mjs +2023 -0
  4. package/fesm2022/mintplayer-ng-spark-auth-account.mjs.map +1 -0
  5. package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs +96 -0
  6. package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs.map +1 -0
  7. package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs +200 -0
  8. package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs.map +1 -0
  9. package/fesm2022/mintplayer-ng-spark-auth-core.mjs +452 -0
  10. package/fesm2022/mintplayer-ng-spark-auth-core.mjs.map +1 -0
  11. package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs +201 -0
  12. package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs.map +1 -0
  13. package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs +196 -0
  14. package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs.map +1 -0
  15. package/fesm2022/mintplayer-ng-spark-auth-guards.mjs +16 -0
  16. package/fesm2022/mintplayer-ng-spark-auth-guards.mjs.map +1 -0
  17. package/fesm2022/mintplayer-ng-spark-auth-interceptors.mjs +18 -0
  18. package/fesm2022/mintplayer-ng-spark-auth-interceptors.mjs.map +1 -0
  19. package/fesm2022/mintplayer-ng-spark-auth-login.mjs +282 -0
  20. package/fesm2022/mintplayer-ng-spark-auth-login.mjs.map +1 -0
  21. package/fesm2022/mintplayer-ng-spark-auth-models.mjs +114 -0
  22. package/fesm2022/mintplayer-ng-spark-auth-models.mjs.map +1 -0
  23. package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs +43 -0
  24. package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs.map +1 -0
  25. package/fesm2022/mintplayer-ng-spark-auth-register.mjs +216 -0
  26. package/fesm2022/mintplayer-ng-spark-auth-register.mjs.map +1 -0
  27. package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs +223 -0
  28. package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs.map +1 -0
  29. package/fesm2022/mintplayer-ng-spark-auth-routes.mjs +194 -0
  30. package/fesm2022/mintplayer-ng-spark-auth-routes.mjs.map +1 -0
  31. package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs +210 -0
  32. package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs.map +1 -0
  33. package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs +218 -0
  34. package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs.map +1 -0
  35. package/fesm2022/mintplayer-ng-spark-auth.mjs +48 -0
  36. package/fesm2022/mintplayer-ng-spark-auth.mjs.map +1 -0
  37. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +294 -2
  38. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  39. package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs +389 -0
  40. package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs.map +1 -0
  41. package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs +204 -0
  42. package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs.map +1 -0
  43. package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs +486 -0
  44. package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs.map +1 -0
  45. package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs +350 -0
  46. package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs.map +1 -0
  47. package/fesm2022/mintplayer-ng-spark-identity-provider.mjs +66 -0
  48. package/fesm2022/mintplayer-ng-spark-identity-provider.mjs.map +1 -0
  49. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +9 -4
  50. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  51. package/fesm2022/mintplayer-ng-spark-pwa.mjs +162 -0
  52. package/fesm2022/mintplayer-ng-spark-pwa.mjs.map +1 -0
  53. package/fesm2022/mintplayer-ng-spark-services.mjs +11 -1
  54. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  55. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  56. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  57. package/package.json +99 -1
  58. package/types/mintplayer-ng-spark-auth-account.d.ts +255 -0
  59. package/types/mintplayer-ng-spark-auth-auth-bar.d.ts +22 -0
  60. package/types/mintplayer-ng-spark-auth-confirm-email.d.ts +21 -0
  61. package/types/mintplayer-ng-spark-auth-core.d.ts +197 -0
  62. package/types/mintplayer-ng-spark-auth-external-login.d.ts +132 -0
  63. package/types/mintplayer-ng-spark-auth-forgot-password.d.ts +18 -0
  64. package/types/mintplayer-ng-spark-auth-guards.d.ts +15 -0
  65. package/types/mintplayer-ng-spark-auth-interceptors.d.ts +2 -0
  66. package/types/mintplayer-ng-spark-auth-login.d.ts +46 -0
  67. package/types/mintplayer-ng-spark-auth-models.d.ts +489 -0
  68. package/types/mintplayer-ng-spark-auth-pipes.d.ts +8 -0
  69. package/types/mintplayer-ng-spark-auth-register.d.ts +21 -0
  70. package/types/mintplayer-ng-spark-auth-reset-password.d.ts +25 -0
  71. package/types/mintplayer-ng-spark-auth-routes.d.ts +154 -0
  72. package/types/mintplayer-ng-spark-auth-sign-in.d.ts +84 -0
  73. package/types/mintplayer-ng-spark-auth-two-factor.d.ts +23 -0
  74. package/types/mintplayer-ng-spark-auth.d.ts +24 -0
  75. package/types/mintplayer-ng-spark-client-operations.d.ts +78 -3
  76. package/types/mintplayer-ng-spark-identity-provider-connected-applications.d.ts +35 -0
  77. package/types/mintplayer-ng-spark-identity-provider-core.d.ts +144 -0
  78. package/types/mintplayer-ng-spark-identity-provider-developers.d.ts +55 -0
  79. package/types/mintplayer-ng-spark-identity-provider-management.d.ts +33 -0
  80. package/types/mintplayer-ng-spark-identity-provider.d.ts +64 -0
  81. package/types/mintplayer-ng-spark-pwa.d.ts +89 -0
  82. package/types/mintplayer-ng-spark-services.d.ts +10 -0
  83. package/types/mintplayer-ng-spark-shell.d.ts +2 -2
  84. package/types/mintplayer-ng-spark.d.ts +4 -4
@@ -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, inject, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { TranslatedString, currentLanguage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\ninterface CultureConfiguration {\n languages: Record<string, TranslatedString>;\n defaultLanguage: string;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class SparkLanguageService {\n private readonly http = inject(HttpClient);\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly currentLang = signal('en');\n private readonly translationsMap = signal<Record<string, TranslatedString>>({});\n\n readonly language = this.currentLang.asReadonly();\n readonly languages = signal<Record<string, TranslatedString>>({});\n\n constructor() {\n this.loadCulture();\n this.loadTranslations();\n }\n\n private async loadCulture(): Promise<void> {\n const config = await firstValueFrom(this.http.get<CultureConfiguration>(`${this.baseUrl}/culture`));\n this.languages.set(config.languages);\n const saved = localStorage.getItem('spark-lang');\n const lang = saved ?? config.defaultLanguage;\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n }\n\n private async loadTranslations(): Promise<void> {\n const t = await firstValueFrom(this.http.get<Record<string, TranslatedString>>(`${this.baseUrl}/translations`));\n this.translationsMap.set(t);\n }\n\n setLanguage(lang: string) {\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n localStorage.setItem('spark-lang', lang);\n }\n\n resolve(ts: TranslatedString | undefined): string {\n if (!ts) return '';\n const lang = this.currentLang();\n return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n }\n\n t(key: string): string {\n const ts = this.translationsMap()[key];\n return this.resolve(ts) || key;\n }\n}\n","import { inject, Injectable, NgZone } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { StreamingMessage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n@Injectable({ providedIn: 'root' })\nexport class SparkStreamingService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly ngZone = inject(NgZone);\n\n connectToStreamingQuery(queryId: string): Observable<StreamingMessage> {\n return new Observable<StreamingMessage>(subscriber => {\n let ws: WebSocket | null = null;\n let retryCount = 0;\n let retryTimeout: ReturnType<typeof setTimeout> | null = null;\n let closed = false;\n const maxRetries = 10;\n const maxDelay = 30000;\n\n const connect = () => {\n if (closed) return;\n\n const url = this.buildWebSocketUrl(queryId);\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n retryCount = 0; // Reset on successful connect\n };\n\n ws.onmessage = (event) => {\n try {\n const message: StreamingMessage = JSON.parse(event.data);\n this.ngZone.run(() => subscriber.next(message));\n } catch {\n // Ignore malformed messages\n }\n };\n\n ws.onerror = () => {\n // Error will trigger onclose\n };\n\n ws.onclose = (event) => {\n if (closed) return;\n\n // Don't reconnect on normal closure\n if (event.code === 1000) {\n this.ngZone.run(() => subscriber.complete());\n return;\n }\n\n // Reconnect with exponential backoff\n if (retryCount < maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, retryCount), maxDelay);\n retryCount++;\n retryTimeout = setTimeout(() => connect(), delay);\n } else {\n this.ngZone.run(() => subscriber.error(new Error('WebSocket connection failed after maximum retries')));\n }\n };\n };\n\n connect();\n\n // Teardown: close WebSocket on unsubscribe\n return () => {\n closed = true;\n if (retryTimeout) {\n clearTimeout(retryTimeout);\n }\n if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {\n ws.close(1000, 'Client unsubscribed');\n }\n };\n });\n }\n\n private buildWebSocketUrl(queryId: string): string {\n const encodedId = encodeURIComponent(queryId);\n const path = `${this.baseUrl}/queries/${encodedId}/stream`;\n\n // If baseUrl is absolute (starts with http/https), replace protocol\n if (this.baseUrl.startsWith('http://')) {\n return path.replace(/^http:\\/\\//, 'ws://');\n }\n if (this.baseUrl.startsWith('https://')) {\n return path.replace(/^https:\\/\\//, 'wss://');\n }\n\n // Relative URL — construct from window.location\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${window.location.host}${path}`;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { HttpClient, HttpErrorResponse } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { CustomActionDefinition, DistinctValuesResult, EntityPermissions, EntityType, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, ProgramUnitsConfiguration, QueryColumnFilter, QueryResult, SparkDeletedFilter, SparkQuery, RetryActionPayload, RetryActionResult, 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-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 +1 @@
1
- {"version":3,"file":"mintplayer-ng-spark-shell.mjs","names":[],"sources":["../../shell/src/spark-program-units.component.ts","../../shell/src/spark-program-units.component.html","../../shell/src/spark-language-selector.component.ts","../../shell/src/spark-shell-slots.ts","../../shell/src/spark-shell.component.ts","../../shell/src/spark-shell.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, effect, inject, input, signal, untracked } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { RouterLink, RouterLinkActive } from '@angular/router';\nimport { BsAccordionComponent, BsAccordionTabComponent, BsAccordionTabHeaderDirective } from '@mintplayer/ng-bootstrap/accordion';\nimport { SPARK_AUTH_STATE } from '@mintplayer/ng-spark';\nimport { ProgramUnitGroup } from '@mintplayer/ng-spark/models';\nimport { SparkService } from '@mintplayer/ng-spark/services';\nimport { IconNamePipe, ResolveTranslationPipe, RouterLinkPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkSidebarTab } from './spark-shell-slots';\n\n/**\n * The server-driven navigation menu: an accordion of program-unit groups fetched from\n * `GET /spark/program-units`, which the server has already filtered to what the caller's rights\n * allow. Hosts write ZERO router links for navigation — every group, unit, icon, label and link\n * comes from `programUnits.json`; content around the menu belongs in `<spark-shell>`'s slots,\n * and a host tempted to hand-write a unit anchor should add a unit to `programUnits.json`\n * instead.\n *\n * Because the response is caller-scoped it must be re-fetched when the caller changes: the\n * component tracks the optional `SPARK_AUTH_STATE` signal (supplied by ng-spark-auth's\n * `provideSparkAuth()`, or by the app's own auth stack) and reloads on every change. Without a\n * provider it fetches once. `reloadToken` is the manual escape hatch (any changed value triggers\n * a reload), and `reload()` the imperative one.\n *\n * Usually rendered by `<spark-shell>`; exported standalone for hosts that own their own layout.\n */\n@Component({\n selector: 'spark-program-units',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n NgTemplateOutlet,\n RouterLink, RouterLinkActive,\n BsAccordionComponent, BsAccordionTabComponent, BsAccordionTabHeaderDirective,\n SparkIconComponent, ResolveTranslationPipe, IconNamePipe, RouterLinkPipe,\n ],\n templateUrl: './spark-program-units.component.html',\n styleUrl: './spark-program-units.component.scss',\n})\nexport class SparkProgramUnitsComponent {\n private readonly sparkService = inject(SparkService);\n private readonly authState = inject(SPARK_AUTH_STATE, { optional: true });\n\n /** Any changed value triggers a reload — for apps whose auth state isn't a provided signal. */\n readonly reloadToken = input<unknown>(null);\n\n /**\n * Extra tabs to render after the generated groups, normally forwarded by `<spark-shell>` from\n * its `*sparkShellTab` directives. They must be rendered by THIS template — see\n * `SparkShellTabDirective` for why a host-declared `<bs-accordion-tab>` cannot work.\n */\n readonly extraTabs = input<readonly SparkSidebarTab[]>([]);\n\n protected readonly groups = signal<ProgramUnitGroup[]>([]);\n\n constructor() {\n effect(() => {\n this.authState?.();\n this.reloadToken();\n untracked(() => this.reload());\n });\n }\n\n /** Re-fetches the menu. The response is already rights-filtered per caller. */\n async reload(): Promise<void> {\n const config = await this.sparkService.getProgramUnits();\n // Order is the server file's contract, but sort both levels here so the rendered menu never\n // depends on JSON array order — groups were sorted, units were not (#324 F5).\n const groups = [...config.programUnitGroups]\n .sort((a, b) => a.order - b.order)\n .map(g => ({ ...g, programUnits: [...g.programUnits].sort((a, b) => a.order - b.order) }));\n this.groups.set(groups);\n }\n}\n","<bs-accordion class=\"d-block\">\n @for (group of groups(); track group.id) {\n <bs-accordion-tab>\n <ng-container *bsAccordionTabHeader>\n <spark-icon [name]=\"group.icon | iconName:'bi-folder'\" />\n <span class=\"ms-2\">{{ group.name | resolveTranslation }}</span>\n </ng-container>\n @for (unit of group.programUnits; track unit.id) {\n @if (unit | routerLink; as link) {\n <a [routerLink]=\"link\"\n routerLinkActive=\"active\"\n class=\"d-block px-3 py-2 text-decoration-none nav-link\">\n <spark-icon [name]=\"unit.icon | iconName:'bi-file'\" />\n <span class=\"ms-2\">{{ unit.name | resolveTranslation }}</span>\n </a>\n } @else {\n <!-- A url unit is an external address: a plain anchor, never a router link. -->\n <a [href]=\"unit.url\" target=\"_blank\" rel=\"noopener\"\n class=\"d-block px-3 py-2 text-decoration-none nav-link\">\n <spark-icon [name]=\"unit.icon | iconName:'bi-box-arrow-up-right'\" />\n <span class=\"ms-2\">{{ unit.name | resolveTranslation }}</span>\n </a>\n }\n }\n </bs-accordion-tab>\n }\n <!-- Host-contributed tabs are rendered HERE, by this template, so that the accordion's content\n query registers them and they join the generated groups' single-open group. -->\n @for (tab of extraTabs(); track tab.content) {\n <bs-accordion-tab>\n <ng-container *bsAccordionTabHeader>\n @if (tab.headerTemplate; as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-icon [name]=\"tab.icon | iconName:'bi-folder'\" />\n <span class=\"ms-2\">{{ tab.header }}</span>\n }\n </ng-container>\n <ng-container *ngTemplateOutlet=\"tab.content\" />\n </bs-accordion-tab>\n }\n</bs-accordion>\n","import { ChangeDetectionStrategy, Component, inject } from '@angular/core';\nimport { KeyValuePipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { BsSelectComponent, BsSelectOption } from '@mintplayer/ng-bootstrap/select';\nimport { SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport { ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\n\n/**\n * The culture switcher: a `bs-select` over `SparkLanguageService`'s languages, persisting the\n * choice. Renders nothing when the app declares one language (or none), so hosts can include it\n * unconditionally — `<spark-shell>`'s topbar does exactly that as its trailing default.\n */\n@Component({\n selector: 'spark-language-selector',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [KeyValuePipe, FormsModule, BsSelectComponent, BsSelectOption, ResolveTranslationPipe],\n template: `\n @if (lang.languages() | keyvalue; as langs) {\n @if (langs.length > 1) {\n <bs-select [ngModel]=\"lang.language()\" (ngModelChange)=\"lang.setLanguage($event)\">\n @for (l of langs; track l.key) {\n <option [ngValue]=\"l.key\">{{ l.value | resolveTranslation }}</option>\n }\n </bs-select>\n }\n }\n `,\n})\nexport class SparkLanguageSelectorComponent {\n protected readonly lang = inject(SparkLanguageService);\n}\n","import { Directive, inject, input, TemplateRef } from '@angular/core';\n\n/**\n * Region slots for `<spark-shell>`.\n *\n * Each directive marks a template that REPLACES one region of the shell chrome (or fills an\n * empty one). An omitted slot is not an empty slot: the shell renders its default — the toggler,\n * the language selector, the title heading — so a host that supplies nothing still gets a\n * complete working shell, and a host that supplies one slot leaves the rest alone.\n *\n * The menu itself is deliberately NOT a slot. Navigation is sourced entirely from\n * `programUnits.json` through the rights-filtered `/spark/program-units` endpoint and re-fetched\n * on sign-in/out; a host that finds itself writing unit anchors in a slot should be adding units\n * to `programUnits.json` instead. Slots exist for the content AROUND the menu: an auth bar, a\n * user chip, branding, a one-off extra link, an alert strip above the routed content.\n *\n * Naming follows the house convention (prefix, component, slot — see `*sparkQueryIcon` in\n * `@mintplayer/ng-spark/grid`): `sparkShell` + region. Every slot also exists as a `TemplateRef`\n * input on `SparkShellComponent` for hosts that cannot use content projection.\n *\n * ```html\n * <spark-shell title=\"My App\">\n * <spark-auth-bar *sparkShellTopbarEnd />\n * <div *sparkShellSidebarTop>\n * <a routerLink=\"/github-projects\" class=\"nav-link\">GitHub projects</a>\n * </div>\n * <router-outlet />\n * </spark-shell>\n * ```\n */\n\n/** Topbar, leading edge. Default: a `bs-navbar-toggler` mirroring the shell's open state. */\n@Directive({ selector: '[sparkShellTopbarStart]' })\nexport class SparkShellTopbarStartDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/**\n * Topbar, trailing edge. Default: the language selector (which hides itself when the app has\n * one language). This is where an auth bar goes — the shell cannot ship one itself, since\n * `@mintplayer/ng-spark` does not (and must not) depend on `@mintplayer/ng-spark-auth`.\n */\n@Directive({ selector: '[sparkShellTopbarEnd]' })\nexport class SparkShellTopbarEndDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/**\n * Topbar, trailing edge, **beside** the default chrome rather than instead of it — rendered before\n * the language selector, which stays.\n *\n * ## Why this is not just `*sparkShellTopbarEnd`\n *\n * That slot REPLACES the region, which is right for an auth bar (a host taking over the trailing\n * edge wholesale) and wrong for a page-level action button. A host that only wanted to add a\n * button had to re-render the language selector itself to keep it — which means importing it,\n * knowing it hides itself in a single-language app, and keeping that copy in step with the shell.\n * Every host that did this got it slightly differently.\n *\n * Pairs with `SparkQueryActionsService`: that service resolves a query's custom actions without the\n * grid, and this is where a page-level one goes.\n *\n * ```html\n * <button *sparkShellTopbarActions class=\"btn btn-primary\" (click)=\"publish()\">Publish</button>\n * ```\n *\n * Supplying both slots is allowed and does what it says: `topbarEnd` replaces the default chrome,\n * and these actions still render ahead of whatever it put there.\n */\n@Directive({ selector: '[sparkShellTopbarActions]' })\nexport class SparkShellTopbarActionsDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/** Sidebar, above everything. Default: `<h5>{{ title }}</h5>`. */\n@Directive({ selector: '[sparkShellSidebarHeader]' })\nexport class SparkShellSidebarHeaderDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/** Sidebar, between the header and the program-units menu. No default. */\n@Directive({ selector: '[sparkShellSidebarTop]' })\nexport class SparkShellSidebarTopDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/**\n * An extra accordion tab in the sidebar menu, rendered after the generated program-unit groups\n * and sharing their single-open behavior.\n *\n * A tab is contributed as DATA (header + body template), not as markup, and that is load-bearing:\n * `bs-accordion` discovers its tabs with an Angular content query, which matches by declaration\n * view, so a `<bs-accordion-tab>` written in a host's template and inserted into the library's\n * accordion is never registered — it would land at index -1, get no hoisted header and no slot.\n * Declaring a second `<bs-accordion>` instead is what puts the tab in its own exclusivity group:\n * `mp-accordion` enforces single-open per element, over children it owns and over\n * `<details name>`, whose grouping cannot cross a shadow root. So the tab element must be created\n * by the menu itself, from what this directive carries.\n *\n * ```html\n * <ng-container *sparkShellTab=\"'Component demos'; icon: 'palette'\">\n * <a routerLink=\"/query-slots\" routerLinkActive=\"active\" class=\"nav-link\">Query card slots</a>\n * </ng-container>\n * ```\n *\n * Navigation still belongs in `programUnits.json` — this is for pages the model cannot describe\n * (client-side demos, external tools). For sidebar content that is NOT an accordion tab, use\n * `*sparkShellSidebarTop` or `*sparkShellSidebarFooter`.\n */\n@Directive({ selector: '[sparkShellTab]' })\nexport class SparkShellTabDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n\n /** The tab's header label. */\n readonly header = input.required<string>({ alias: 'sparkShellTab' });\n\n /** Bootstrap icon name for the header, as in `programUnits.json`. Defaults to a folder. */\n readonly icon = input<string | undefined>(undefined, { alias: 'sparkShellTabIcon' });\n\n /** Replaces the icon+label header entirely, for a header that needs its own markup. */\n readonly headerTemplate = input<TemplateRef<unknown> | null>(null, { alias: 'sparkShellTabHeader' });\n}\n\n/**\n * A sidebar accordion tab in the shape the menu renders it. Hosts normally contribute tabs with\n * `*sparkShellTab`; this is the same thing as data, for a host that computes its tabs.\n */\nexport interface SparkSidebarTab {\n readonly header: string;\n readonly icon?: string;\n readonly headerTemplate?: TemplateRef<unknown> | null;\n readonly content: TemplateRef<unknown>;\n}\n\n/** Sidebar, at the very bottom. No default. */\n@Directive({ selector: '[sparkShellSidebarFooter]' })\nexport class SparkShellSidebarFooterDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/** Main region, above the projected content (the host's `<router-outlet>`). No default. */\n@Directive({ selector: '[sparkShellMainHeader]' })\nexport class SparkShellMainHeaderDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n","import { ChangeDetectionStrategy, Component, computed, contentChild, contentChildren, inject, input, signal, TemplateRef } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { BsShellComponent, BsShellSidebarDirective, BsShellState } from '@mintplayer/ng-bootstrap/shell';\nimport { BsNavbarTogglerComponent } from '@mintplayer/ng-bootstrap/navbar-toggler';\nimport { BS_THEME_DEFAULT_MODES, BsThemeService, BsThemeToggleComponent, BsThemeToggleMode } from '@mintplayer/ng-bootstrap/theming';\nimport { SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport type { Breakpoint } from '@mintplayer/ng-bootstrap';\nimport type { ShellStateChangeEventDetail } from '@mintplayer/web-components/shell';\nimport { SparkProgramUnitsComponent } from './spark-program-units.component';\nimport { SparkLanguageSelectorComponent } from './spark-language-selector.component';\nimport {\n SparkShellMainHeaderDirective,\n SparkShellSidebarFooterDirective,\n SparkShellSidebarHeaderDirective,\n SparkShellSidebarTopDirective,\n SparkShellTabDirective,\n SparkShellTopbarEndDirective,\n SparkShellTopbarActionsDirective,\n SparkShellTopbarStartDirective,\n SparkSidebarTab,\n} from './spark-shell-slots';\n\n/**\n * The application frame: topbar + sidebar + main, wrapping ng-bootstrap's `bs-shell` (whose\n * `mp-shell` web component owns ALL responsive behavior — breakpoints, the overlay drawer,\n * dismiss-on-navigate — in CSS; nothing here re-derives a pixel width). The sidebar renders the\n * server-driven program-units menu; the host projects its `<router-outlet>` as the default\n * content and customizes the chrome through the `*sparkShell*` slots (see `spark-shell-slots.ts`\n * for the doctrine: an omitted slot renders its default, and the menu itself is never a slot).\n *\n * ```html\n * <spark-shell title=\"My App\">\n * <spark-auth-bar *sparkShellTopbarEnd />\n * <router-outlet />\n * </spark-shell>\n * ```\n *\n * The one piece of state the shell keeps is the toggler↔drawer mirror: the built-in hamburger is\n * hidden (`::part(hamburger)`) in favor of a `bs-navbar-toggler` in the topbar, so the shell\n * listens to `statechange` to keep the toggler's icon truthful in `auto` mode and only forces\n * `show`/`hide` on explicit toggles.\n *\n * Theming: the chrome colors are CSS custom properties —\n * `--spark-shell-{topbar,sidebar,main}-{bg,color}` — declared per colour scheme under\n * `[data-bs-theme=light]` / `[data-bs-theme=dark]` from Bootstrap tokens, so the whole shell follows\n * the page's theme. An app restyles it by overriding those tokens, globally per scheme or on the\n * `<spark-shell>` element.\n *\n * Colour mode: the shell injects `BsThemeService` itself, so 'auto' follows the OS live even when\n * the topbar toggle is hidden with `[themeToggle]=\"false\"` (an app may then place its own\n * `<bs-theme-toggle>` elsewhere). The first paint is themed by `bs-theme-preboot.js` in the app's\n * `index.html`, not by anything here.\n */\n@Component({\n selector: 'spark-shell',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n NgTemplateOutlet,\n BsShellComponent, BsShellSidebarDirective, BsNavbarTogglerComponent, BsThemeToggleComponent,\n SparkProgramUnitsComponent, SparkLanguageSelectorComponent,\n ],\n templateUrl: './spark-shell.component.html',\n styleUrl: './spark-shell.component.scss',\n})\nexport class SparkShellComponent {\n /** The sidebar heading. Ignored when a `*sparkShellSidebarHeader` slot is supplied. */\n readonly title = input('');\n\n /** Forwarded to `bs-shell`: below it the sidebar is an overlay drawer. */\n readonly breakpoint = input<Breakpoint>('md');\n\n /**\n * Render the Auto / Light / Dark `bs-theme-toggle` in the topbar. `false` hides it; the theme\n * service stays live either way.\n */\n readonly themeToggle = input(true);\n\n /**\n * Instantiated here, not only through the toggle (PRD D3): without it, `[themeToggle]=\"false\"`\n * would leave 'auto' with no live `prefers-color-scheme` listener in Angular's view.\n */\n protected readonly theme = inject(BsThemeService);\n private readonly lang = inject(SparkLanguageService);\n\n /** The toggle's cycle with Spark's translated labels (`theme.*` keys); icons stay ng-bootstrap's. */\n protected readonly themeModes = computed<readonly BsThemeToggleMode[]>(() =>\n BS_THEME_DEFAULT_MODES.map(m => {\n const name = m.mode.charAt(0).toUpperCase() + m.mode.slice(1);\n return { ...m, label: this.lang.t(`theme.switchTo${name}`), announcement: this.lang.t(`theme.${m.mode}`) };\n }));\n\n /** Forwarded to the menu: any changed value re-fetches the program units. */\n readonly reloadToken = input<unknown>(null);\n\n /** Extra sidebar tabs as data, for hosts that compute them; `*sparkShellTab` is the usual way. */\n readonly sidebarTabs = input<readonly SparkSidebarTab[]>([]);\n\n // One TemplateRef input per slot, for hosts that cannot use content projection\n // (the spark-query-card precedent). The projected directive wins when both are present.\n readonly topbarStartTemplate = input<TemplateRef<unknown> | null>(null);\n readonly topbarEndTemplate = input<TemplateRef<unknown> | null>(null);\n readonly topbarActionsTemplate = input<TemplateRef<unknown> | null>(null);\n readonly sidebarHeaderTemplate = input<TemplateRef<unknown> | null>(null);\n readonly sidebarTopTemplate = input<TemplateRef<unknown> | null>(null);\n readonly sidebarFooterTemplate = input<TemplateRef<unknown> | null>(null);\n readonly mainHeaderTemplate = input<TemplateRef<unknown> | null>(null);\n\n private readonly topbarStartSlot = contentChild(SparkShellTopbarStartDirective);\n private readonly topbarEndSlot = contentChild(SparkShellTopbarEndDirective);\n private readonly topbarActionsSlot = contentChild(SparkShellTopbarActionsDirective);\n private readonly sidebarHeaderSlot = contentChild(SparkShellSidebarHeaderDirective);\n private readonly sidebarTopSlot = contentChild(SparkShellSidebarTopDirective);\n private readonly sidebarFooterSlot = contentChild(SparkShellSidebarFooterDirective);\n private readonly mainHeaderSlot = contentChild(SparkShellMainHeaderDirective);\n\n protected readonly topbarStartTpl = computed(() => this.topbarStartSlot()?.templateRef ?? this.topbarStartTemplate());\n protected readonly topbarEndTpl = computed(() => this.topbarEndSlot()?.templateRef ?? this.topbarEndTemplate());\n protected readonly topbarActionsTpl = computed(() => this.topbarActionsSlot()?.templateRef ?? this.topbarActionsTemplate());\n protected readonly sidebarHeaderTpl = computed(() => this.sidebarHeaderSlot()?.templateRef ?? this.sidebarHeaderTemplate());\n protected readonly sidebarTopTpl = computed(() => this.sidebarTopSlot()?.templateRef ?? this.sidebarTopTemplate());\n protected readonly sidebarFooterTpl = computed(() => this.sidebarFooterSlot()?.templateRef ?? this.sidebarFooterTemplate());\n protected readonly mainHeaderTpl = computed(() => this.mainHeaderSlot()?.templateRef ?? this.mainHeaderTemplate());\n\n /**\n * Extra accordion tabs, forwarded to the menu so IT creates the `<bs-accordion-tab>` elements —\n * the only way they share the generated groups' single-open behavior (see\n * `SparkShellTabDirective`). Data-supplied tabs come first, then projected ones in declaration\n * order.\n */\n private readonly tabSlots = contentChildren(SparkShellTabDirective);\n\n protected readonly tabs = computed<readonly SparkSidebarTab[]>(() => [\n ...this.sidebarTabs(),\n ...this.tabSlots().map(slot => ({\n header: slot.header(),\n icon: slot.icon(),\n headerTemplate: slot.headerTemplate(),\n content: slot.templateRef,\n })),\n ]);\n\n protected readonly shellState = signal<BsShellState>('auto');\n protected readonly isSidebarVisible = signal(false);\n\n // Explicit toggles force show/hide; 'auto' responsive behavior is otherwise preserved by\n // never writing state on our own. The mirror below keeps the toggler icon truthful when the\n // shell opens/closes itself at the breakpoint.\n protected toggleSidebar(open: boolean): void {\n this.shellState.set(open ? 'show' : 'hide');\n }\n\n protected onShellToggle(detail: ShellStateChangeEventDetail): void {\n this.isSidebarVisible.set(detail.open);\n }\n}\n","<bs-shell [state]=\"shellState()\" [breakpoint]=\"breakpoint()\" [dismissOnNavigate]=\"true\"\n (statechange)=\"onShellToggle($event)\">\n <div slot=\"topbar\" class=\"spark-topbar w-100 d-flex align-items-center\">\n @if (topbarStartTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <bs-navbar-toggler [state]=\"isSidebarVisible()\" (stateChange)=\"toggleSidebar($event)\" />\n }\n <div class=\"ms-auto me-3 d-flex align-items-center gap-2\">\n <!-- Ahead of the trailing chrome, and OUTSIDE the @if below: these actions are additive, so\n they render whether the host replaced the trailing edge or left the default selector. -->\n @if (topbarActionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <!-- Also outside the trailing @if: replacing the end slot (e.g. with spark-auth-bar) keeps it. -->\n @if (themeToggle()) {\n <bs-theme-toggle [modes]=\"themeModes()\" />\n }\n @if (topbarEndTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-language-selector />\n }\n </div>\n </div>\n\n <nav bsShellSidebar class=\"spark-sidebar p-2\">\n @if (sidebarHeaderTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <h5 class=\"p-3 border-bottom\">{{ title() }}</h5>\n }\n @if (sidebarTopTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <spark-program-units [reloadToken]=\"reloadToken()\" [extraTabs]=\"tabs()\" />\n @if (sidebarFooterTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n </nav>\n\n <!-- The padding lives in the stylesheet as `--spark-main-padding`, not as a `p-4` utility, so the\n sticky action bar can read it and bleed back out to the scrollport edges. See _actionbar.scss. -->\n <main>\n @if (mainHeaderTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <ng-content />\n </main>\n</bs-shell>\n"],"mappings":"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"}
1
+ {"version":3,"file":"mintplayer-ng-spark-shell.mjs","names":[],"sources":["../../shell/src/spark-program-units.component.ts","../../shell/src/spark-program-units.component.html","../../shell/src/spark-language-selector.component.ts","../../shell/src/spark-shell-slots.ts","../../shell/src/spark-shell.component.ts","../../shell/src/spark-shell.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, effect, inject, input, signal, untracked } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { RouterLink, RouterLinkActive } from '@angular/router';\nimport { BsAccordionComponent, BsAccordionTabComponent, BsAccordionTabHeaderDirective } from '@mintplayer/ng-bootstrap/accordion';\nimport { SPARK_AUTH_STATE } from '@mintplayer/ng-spark';\nimport { ProgramUnitGroup } from '@mintplayer/ng-spark/models';\nimport { SparkService } from '@mintplayer/ng-spark/services';\nimport { IconNamePipe, ResolveTranslationPipe, RouterLinkPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkSidebarTab } from './spark-shell-slots';\n\n/**\n * The server-driven navigation menu: an accordion of program-unit groups fetched from\n * `GET /spark/program-units`, which the server has already filtered to what the caller's rights\n * allow. Hosts write ZERO router links for navigation — every group, unit, icon, label and link\n * comes from `programUnits.json`; content around the menu belongs in `<spark-shell>`'s slots,\n * and a host tempted to hand-write a unit anchor should add a unit to `programUnits.json`\n * instead.\n *\n * Because the response is caller-scoped it must be re-fetched when the caller changes: the\n * component tracks the optional `SPARK_AUTH_STATE` signal (supplied by ng-spark/auth's\n * `provideSparkAuth()`, or by the app's own auth stack) and reloads on every change. Without a\n * provider it fetches once. `reloadToken` is the manual escape hatch (any changed value triggers\n * a reload), and `reload()` the imperative one.\n *\n * Usually rendered by `<spark-shell>`; exported standalone for hosts that own their own layout.\n */\n@Component({\n selector: 'spark-program-units',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n NgTemplateOutlet,\n RouterLink, RouterLinkActive,\n BsAccordionComponent, BsAccordionTabComponent, BsAccordionTabHeaderDirective,\n SparkIconComponent, ResolveTranslationPipe, IconNamePipe, RouterLinkPipe,\n ],\n templateUrl: './spark-program-units.component.html',\n styleUrl: './spark-program-units.component.scss',\n})\nexport class SparkProgramUnitsComponent {\n private readonly sparkService = inject(SparkService);\n private readonly authState = inject(SPARK_AUTH_STATE, { optional: true });\n\n /** Any changed value triggers a reload — for apps whose auth state isn't a provided signal. */\n readonly reloadToken = input<unknown>(null);\n\n /**\n * Extra tabs to render after the generated groups, normally forwarded by `<spark-shell>` from\n * its `*sparkShellTab` directives. They must be rendered by THIS template — see\n * `SparkShellTabDirective` for why a host-declared `<bs-accordion-tab>` cannot work.\n */\n readonly extraTabs = input<readonly SparkSidebarTab[]>([]);\n\n protected readonly groups = signal<ProgramUnitGroup[]>([]);\n\n constructor() {\n effect(() => {\n this.authState?.();\n this.reloadToken();\n untracked(() => this.reload());\n });\n }\n\n /** Re-fetches the menu. The response is already rights-filtered per caller. */\n async reload(): Promise<void> {\n const config = await this.sparkService.getProgramUnits();\n // Order is the server file's contract, but sort both levels here so the rendered menu never\n // depends on JSON array order — groups were sorted, units were not (#324 F5).\n const groups = [...config.programUnitGroups]\n .sort((a, b) => a.order - b.order)\n .map(g => ({ ...g, programUnits: [...g.programUnits].sort((a, b) => a.order - b.order) }));\n this.groups.set(groups);\n }\n}\n","<bs-accordion class=\"d-block\">\n @for (group of groups(); track group.id) {\n <bs-accordion-tab>\n <ng-container *bsAccordionTabHeader>\n <spark-icon [name]=\"group.icon | iconName:'bi-folder'\" />\n <span class=\"ms-2\">{{ group.name | resolveTranslation }}</span>\n </ng-container>\n @for (unit of group.programUnits; track unit.id) {\n @if (unit | routerLink; as link) {\n <a [routerLink]=\"link\"\n routerLinkActive=\"active\"\n class=\"d-block px-3 py-2 text-decoration-none nav-link\">\n <spark-icon [name]=\"unit.icon | iconName:'bi-file'\" />\n <span class=\"ms-2\">{{ unit.name | resolveTranslation }}</span>\n </a>\n } @else {\n <!-- A url unit is an external address: a plain anchor, never a router link. -->\n <a [href]=\"unit.url\" target=\"_blank\" rel=\"noopener\"\n class=\"d-block px-3 py-2 text-decoration-none nav-link\">\n <spark-icon [name]=\"unit.icon | iconName:'bi-box-arrow-up-right'\" />\n <span class=\"ms-2\">{{ unit.name | resolveTranslation }}</span>\n </a>\n }\n }\n </bs-accordion-tab>\n }\n <!-- Host-contributed tabs are rendered HERE, by this template, so that the accordion's content\n query registers them and they join the generated groups' single-open group. -->\n @for (tab of extraTabs(); track tab.content) {\n <bs-accordion-tab>\n <ng-container *bsAccordionTabHeader>\n @if (tab.headerTemplate; as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-icon [name]=\"tab.icon | iconName:'bi-folder'\" />\n <span class=\"ms-2\">{{ tab.header }}</span>\n }\n </ng-container>\n <ng-container *ngTemplateOutlet=\"tab.content\" />\n </bs-accordion-tab>\n }\n</bs-accordion>\n","import { ChangeDetectionStrategy, Component, inject } from '@angular/core';\nimport { KeyValuePipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { BsSelectComponent, BsSelectOption } from '@mintplayer/ng-bootstrap/select';\nimport { SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport { ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\n\n/**\n * The culture switcher: a `bs-select` over `SparkLanguageService`'s languages, persisting the\n * choice. Renders nothing when the app declares one language (or none), so hosts can include it\n * unconditionally — `<spark-shell>`'s topbar does exactly that as its trailing default.\n */\n@Component({\n selector: 'spark-language-selector',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [KeyValuePipe, FormsModule, BsSelectComponent, BsSelectOption, ResolveTranslationPipe],\n template: `\n @if (lang.languages() | keyvalue; as langs) {\n @if (langs.length > 1) {\n <bs-select [ngModel]=\"lang.language()\" (ngModelChange)=\"lang.setLanguage($event)\">\n @for (l of langs; track l.key) {\n <option [ngValue]=\"l.key\">{{ l.value | resolveTranslation }}</option>\n }\n </bs-select>\n }\n }\n `,\n})\nexport class SparkLanguageSelectorComponent {\n protected readonly lang = inject(SparkLanguageService);\n}\n","import { Directive, inject, input, TemplateRef } from '@angular/core';\n\n/**\n * Region slots for `<spark-shell>`.\n *\n * Each directive marks a template that REPLACES one region of the shell chrome (or fills an\n * empty one). An omitted slot is not an empty slot: the shell renders its default — the toggler,\n * the language selector, the title heading — so a host that supplies nothing still gets a\n * complete working shell, and a host that supplies one slot leaves the rest alone.\n *\n * The menu itself is deliberately NOT a slot. Navigation is sourced entirely from\n * `programUnits.json` through the rights-filtered `/spark/program-units` endpoint and re-fetched\n * on sign-in/out; a host that finds itself writing unit anchors in a slot should be adding units\n * to `programUnits.json` instead. Slots exist for the content AROUND the menu: an auth bar, a\n * user chip, branding, a one-off extra link, an alert strip above the routed content.\n *\n * Naming follows the house convention (prefix, component, slot — see `*sparkQueryIcon` in\n * `@mintplayer/ng-spark/grid`): `sparkShell` + region. Every slot also exists as a `TemplateRef`\n * input on `SparkShellComponent` for hosts that cannot use content projection.\n *\n * ```html\n * <spark-shell title=\"My App\">\n * <spark-auth-bar *sparkShellTopbarEnd />\n * <div *sparkShellSidebarTop>\n * <a routerLink=\"/github-projects\" class=\"nav-link\">GitHub projects</a>\n * </div>\n * <router-outlet />\n * </spark-shell>\n * ```\n */\n\n/** Topbar, leading edge. Default: a `bs-navbar-toggler` mirroring the shell's open state. */\n@Directive({ selector: '[sparkShellTopbarStart]' })\nexport class SparkShellTopbarStartDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/**\n * Topbar, trailing edge. Default: the language selector (which hides itself when the app has\n * one language). This is where an auth bar goes — the shell cannot ship one itself, since\n * the non-auth entry points of `@mintplayer/ng-spark` do not (and must not) import `@mintplayer/ng-spark/auth/*`.\n */\n@Directive({ selector: '[sparkShellTopbarEnd]' })\nexport class SparkShellTopbarEndDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/**\n * Topbar, trailing edge, **beside** the default chrome rather than instead of it — rendered before\n * the language selector, which stays.\n *\n * ## Why this is not just `*sparkShellTopbarEnd`\n *\n * That slot REPLACES the region, which is right for an auth bar (a host taking over the trailing\n * edge wholesale) and wrong for a page-level action button. A host that only wanted to add a\n * button had to re-render the language selector itself to keep it — which means importing it,\n * knowing it hides itself in a single-language app, and keeping that copy in step with the shell.\n * Every host that did this got it slightly differently.\n *\n * Pairs with `SparkQueryActionsService`: that service resolves a query's custom actions without the\n * grid, and this is where a page-level one goes.\n *\n * ```html\n * <button *sparkShellTopbarActions class=\"btn btn-primary\" (click)=\"publish()\">Publish</button>\n * ```\n *\n * Supplying both slots is allowed and does what it says: `topbarEnd` replaces the default chrome,\n * and these actions still render ahead of whatever it put there.\n */\n@Directive({ selector: '[sparkShellTopbarActions]' })\nexport class SparkShellTopbarActionsDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/** Sidebar, above everything. Default: `<h5>{{ title }}</h5>`. */\n@Directive({ selector: '[sparkShellSidebarHeader]' })\nexport class SparkShellSidebarHeaderDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/** Sidebar, between the header and the program-units menu. No default. */\n@Directive({ selector: '[sparkShellSidebarTop]' })\nexport class SparkShellSidebarTopDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/**\n * An extra accordion tab in the sidebar menu, rendered after the generated program-unit groups\n * and sharing their single-open behavior.\n *\n * A tab is contributed as DATA (header + body template), not as markup, and that is load-bearing:\n * `bs-accordion` discovers its tabs with an Angular content query, which matches by declaration\n * view, so a `<bs-accordion-tab>` written in a host's template and inserted into the library's\n * accordion is never registered — it would land at index -1, get no hoisted header and no slot.\n * Declaring a second `<bs-accordion>` instead is what puts the tab in its own exclusivity group:\n * `mp-accordion` enforces single-open per element, over children it owns and over\n * `<details name>`, whose grouping cannot cross a shadow root. So the tab element must be created\n * by the menu itself, from what this directive carries.\n *\n * ```html\n * <ng-container *sparkShellTab=\"'Component demos'; icon: 'palette'\">\n * <a routerLink=\"/query-slots\" routerLinkActive=\"active\" class=\"nav-link\">Query card slots</a>\n * </ng-container>\n * ```\n *\n * Navigation still belongs in `programUnits.json` — this is for pages the model cannot describe\n * (client-side demos, external tools). For sidebar content that is NOT an accordion tab, use\n * `*sparkShellSidebarTop` or `*sparkShellSidebarFooter`.\n */\n@Directive({ selector: '[sparkShellTab]' })\nexport class SparkShellTabDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n\n /** The tab's header label. */\n readonly header = input.required<string>({ alias: 'sparkShellTab' });\n\n /** Bootstrap icon name for the header, as in `programUnits.json`. Defaults to a folder. */\n readonly icon = input<string | undefined>(undefined, { alias: 'sparkShellTabIcon' });\n\n /** Replaces the icon+label header entirely, for a header that needs its own markup. */\n readonly headerTemplate = input<TemplateRef<unknown> | null>(null, { alias: 'sparkShellTabHeader' });\n}\n\n/**\n * A sidebar accordion tab in the shape the menu renders it. Hosts normally contribute tabs with\n * `*sparkShellTab`; this is the same thing as data, for a host that computes its tabs.\n */\nexport interface SparkSidebarTab {\n readonly header: string;\n readonly icon?: string;\n readonly headerTemplate?: TemplateRef<unknown> | null;\n readonly content: TemplateRef<unknown>;\n}\n\n/** Sidebar, at the very bottom. No default. */\n@Directive({ selector: '[sparkShellSidebarFooter]' })\nexport class SparkShellSidebarFooterDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n\n/** Main region, above the projected content (the host's `<router-outlet>`). No default. */\n@Directive({ selector: '[sparkShellMainHeader]' })\nexport class SparkShellMainHeaderDirective {\n readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);\n}\n","import { ChangeDetectionStrategy, Component, computed, contentChild, contentChildren, inject, input, signal, TemplateRef } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { BsShellComponent, BsShellSidebarDirective, BsShellState } from '@mintplayer/ng-bootstrap/shell';\nimport { BsNavbarTogglerComponent } from '@mintplayer/ng-bootstrap/navbar-toggler';\nimport { BS_THEME_DEFAULT_MODES, BsThemeService, BsThemeToggleComponent, BsThemeToggleMode } from '@mintplayer/ng-bootstrap/theming';\nimport { SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport type { Breakpoint } from '@mintplayer/ng-bootstrap';\nimport type { ShellStateChangeEventDetail } from '@mintplayer/web-components/shell';\nimport { SparkProgramUnitsComponent } from './spark-program-units.component';\nimport { SparkLanguageSelectorComponent } from './spark-language-selector.component';\nimport {\n SparkShellMainHeaderDirective,\n SparkShellSidebarFooterDirective,\n SparkShellSidebarHeaderDirective,\n SparkShellSidebarTopDirective,\n SparkShellTabDirective,\n SparkShellTopbarEndDirective,\n SparkShellTopbarActionsDirective,\n SparkShellTopbarStartDirective,\n SparkSidebarTab,\n} from './spark-shell-slots';\n\n/**\n * The application frame: topbar + sidebar + main, wrapping ng-bootstrap's `bs-shell` (whose\n * `mp-shell` web component owns ALL responsive behavior — breakpoints, the overlay drawer,\n * dismiss-on-navigate — in CSS; nothing here re-derives a pixel width). The sidebar renders the\n * server-driven program-units menu; the host projects its `<router-outlet>` as the default\n * content and customizes the chrome through the `*sparkShell*` slots (see `spark-shell-slots.ts`\n * for the doctrine: an omitted slot renders its default, and the menu itself is never a slot).\n *\n * ```html\n * <spark-shell title=\"My App\">\n * <spark-auth-bar *sparkShellTopbarEnd />\n * <router-outlet />\n * </spark-shell>\n * ```\n *\n * The one piece of state the shell keeps is the toggler↔drawer mirror: the built-in hamburger is\n * hidden (`::part(hamburger)`) in favor of a `bs-navbar-toggler` in the topbar, so the shell\n * listens to `statechange` to keep the toggler's icon truthful in `auto` mode and only forces\n * `show`/`hide` on explicit toggles.\n *\n * Theming: the chrome colors are CSS custom properties —\n * `--spark-shell-{topbar,sidebar,main}-{bg,color}` — declared per colour scheme under\n * `[data-bs-theme=light]` / `[data-bs-theme=dark]` from Bootstrap tokens, so the whole shell follows\n * the page's theme. An app restyles it by overriding those tokens, globally per scheme or on the\n * `<spark-shell>` element.\n *\n * Colour mode: the shell injects `BsThemeService` itself, so 'auto' follows the OS live even when\n * the topbar toggle is hidden with `[themeToggle]=\"false\"` (an app may then place its own\n * `<bs-theme-toggle>` elsewhere). The first paint is themed by `bs-theme-preboot.js` in the app's\n * `index.html`, not by anything here.\n */\n@Component({\n selector: 'spark-shell',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n NgTemplateOutlet,\n BsShellComponent, BsShellSidebarDirective, BsNavbarTogglerComponent, BsThemeToggleComponent,\n SparkProgramUnitsComponent, SparkLanguageSelectorComponent,\n ],\n templateUrl: './spark-shell.component.html',\n styleUrl: './spark-shell.component.scss',\n})\nexport class SparkShellComponent {\n /** The sidebar heading. Ignored when a `*sparkShellSidebarHeader` slot is supplied. */\n readonly title = input('');\n\n /** Forwarded to `bs-shell`: below it the sidebar is an overlay drawer. */\n readonly breakpoint = input<Breakpoint>('md');\n\n /**\n * Render the Auto / Light / Dark `bs-theme-toggle` in the topbar. `false` hides it; the theme\n * service stays live either way.\n */\n readonly themeToggle = input(true);\n\n /**\n * Instantiated here, not only through the toggle (PRD D3): without it, `[themeToggle]=\"false\"`\n * would leave 'auto' with no live `prefers-color-scheme` listener in Angular's view.\n */\n protected readonly theme = inject(BsThemeService);\n private readonly lang = inject(SparkLanguageService);\n\n /** The toggle's cycle with Spark's translated labels (`theme.*` keys); icons stay ng-bootstrap's. */\n protected readonly themeModes = computed<readonly BsThemeToggleMode[]>(() =>\n BS_THEME_DEFAULT_MODES.map(m => {\n const name = m.mode.charAt(0).toUpperCase() + m.mode.slice(1);\n return { ...m, label: this.lang.t(`theme.switchTo${name}`), announcement: this.lang.t(`theme.${m.mode}`) };\n }));\n\n /** Forwarded to the menu: any changed value re-fetches the program units. */\n readonly reloadToken = input<unknown>(null);\n\n /** Extra sidebar tabs as data, for hosts that compute them; `*sparkShellTab` is the usual way. */\n readonly sidebarTabs = input<readonly SparkSidebarTab[]>([]);\n\n // One TemplateRef input per slot, for hosts that cannot use content projection\n // (the spark-query-card precedent). The projected directive wins when both are present.\n readonly topbarStartTemplate = input<TemplateRef<unknown> | null>(null);\n readonly topbarEndTemplate = input<TemplateRef<unknown> | null>(null);\n readonly topbarActionsTemplate = input<TemplateRef<unknown> | null>(null);\n readonly sidebarHeaderTemplate = input<TemplateRef<unknown> | null>(null);\n readonly sidebarTopTemplate = input<TemplateRef<unknown> | null>(null);\n readonly sidebarFooterTemplate = input<TemplateRef<unknown> | null>(null);\n readonly mainHeaderTemplate = input<TemplateRef<unknown> | null>(null);\n\n private readonly topbarStartSlot = contentChild(SparkShellTopbarStartDirective);\n private readonly topbarEndSlot = contentChild(SparkShellTopbarEndDirective);\n private readonly topbarActionsSlot = contentChild(SparkShellTopbarActionsDirective);\n private readonly sidebarHeaderSlot = contentChild(SparkShellSidebarHeaderDirective);\n private readonly sidebarTopSlot = contentChild(SparkShellSidebarTopDirective);\n private readonly sidebarFooterSlot = contentChild(SparkShellSidebarFooterDirective);\n private readonly mainHeaderSlot = contentChild(SparkShellMainHeaderDirective);\n\n protected readonly topbarStartTpl = computed(() => this.topbarStartSlot()?.templateRef ?? this.topbarStartTemplate());\n protected readonly topbarEndTpl = computed(() => this.topbarEndSlot()?.templateRef ?? this.topbarEndTemplate());\n protected readonly topbarActionsTpl = computed(() => this.topbarActionsSlot()?.templateRef ?? this.topbarActionsTemplate());\n protected readonly sidebarHeaderTpl = computed(() => this.sidebarHeaderSlot()?.templateRef ?? this.sidebarHeaderTemplate());\n protected readonly sidebarTopTpl = computed(() => this.sidebarTopSlot()?.templateRef ?? this.sidebarTopTemplate());\n protected readonly sidebarFooterTpl = computed(() => this.sidebarFooterSlot()?.templateRef ?? this.sidebarFooterTemplate());\n protected readonly mainHeaderTpl = computed(() => this.mainHeaderSlot()?.templateRef ?? this.mainHeaderTemplate());\n\n /**\n * Extra accordion tabs, forwarded to the menu so IT creates the `<bs-accordion-tab>` elements —\n * the only way they share the generated groups' single-open behavior (see\n * `SparkShellTabDirective`). Data-supplied tabs come first, then projected ones in declaration\n * order.\n */\n private readonly tabSlots = contentChildren(SparkShellTabDirective);\n\n protected readonly tabs = computed<readonly SparkSidebarTab[]>(() => [\n ...this.sidebarTabs(),\n ...this.tabSlots().map(slot => ({\n header: slot.header(),\n icon: slot.icon(),\n headerTemplate: slot.headerTemplate(),\n content: slot.templateRef,\n })),\n ]);\n\n protected readonly shellState = signal<BsShellState>('auto');\n protected readonly isSidebarVisible = signal(false);\n\n // Explicit toggles force show/hide; 'auto' responsive behavior is otherwise preserved by\n // never writing state on our own. The mirror below keeps the toggler icon truthful when the\n // shell opens/closes itself at the breakpoint.\n protected toggleSidebar(open: boolean): void {\n this.shellState.set(open ? 'show' : 'hide');\n }\n\n protected onShellToggle(detail: ShellStateChangeEventDetail): void {\n this.isSidebarVisible.set(detail.open);\n }\n}\n","<bs-shell [state]=\"shellState()\" [breakpoint]=\"breakpoint()\" [dismissOnNavigate]=\"true\"\n (statechange)=\"onShellToggle($event)\">\n <div slot=\"topbar\" class=\"spark-topbar w-100 d-flex align-items-center\">\n @if (topbarStartTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <bs-navbar-toggler [state]=\"isSidebarVisible()\" (stateChange)=\"toggleSidebar($event)\" />\n }\n <div class=\"ms-auto me-3 d-flex align-items-center gap-2\">\n <!-- Ahead of the trailing chrome, and OUTSIDE the @if below: these actions are additive, so\n they render whether the host replaced the trailing edge or left the default selector. -->\n @if (topbarActionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <!-- Also outside the trailing @if: replacing the end slot (e.g. with spark-auth-bar) keeps it. -->\n @if (themeToggle()) {\n <bs-theme-toggle [modes]=\"themeModes()\" />\n }\n @if (topbarEndTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-language-selector />\n }\n </div>\n </div>\n\n <nav bsShellSidebar class=\"spark-sidebar p-2\">\n @if (sidebarHeaderTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <h5 class=\"p-3 border-bottom\">{{ title() }}</h5>\n }\n @if (sidebarTopTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <spark-program-units [reloadToken]=\"reloadToken()\" [extraTabs]=\"tabs()\" />\n @if (sidebarFooterTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n </nav>\n\n <!-- The padding lives in the stylesheet as `--spark-main-padding`, not as a `p-4` utility, so the\n sticky action bar can read it and bleed back out to the scrollport edges. See _actionbar.scss. -->\n <main>\n @if (mainHeaderTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <ng-content />\n </main>\n</bs-shell>\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"mintplayer-ng-spark.mjs","names":[],"sources":["../../src/lib/spark-config.ts","../../src/lib/spark-auth-state.ts","../../src/lib/provide-spark.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\n\nexport interface SparkConfig {\n baseUrl: string;\n /**\n * The concurrency-conflict notice and dialog on the edit page.\n *\n * `showChangedBy`: when true, \"Changed by X\" names whoever wrote the version merged against, but\n * only when History resolves a display name for exactly that version (a `History/T` holder; one\n * `POST /spark/po/revisions` request). Default false: no History request is made and no user is\n * shown. A raw `ModifiedBy` user id is never shown in either mode. The \"at T\" time part is shown\n * whenever the object carries `ModifiedAt`.\n */\n conflictDialog?: { showChangedBy?: boolean };\n}\n\nexport const SPARK_CONFIG = new InjectionToken<SparkConfig>('SPARK_CONFIG');\n\nexport const defaultSparkConfig: SparkConfig = {\n baseUrl: '/spark',\n conflictDialog: { showChangedBy: false },\n};\n","import { InjectionToken, Signal } from '@angular/core';\n\n/**\n * A signal that changes whenever the authenticated user changes — the bridge that lets ng-spark\n * components react to sign-in/out without a dependency on `@mintplayer/ng-spark-auth` (no\n * dependency exists between the two packages, in either direction, on purpose).\n *\n * `@mintplayer/ng-spark-auth`'s `provideSparkAuth()` supplies it from `SparkAuthService.user`;\n * an app with its own auth stack provides any signal that changes on sign-in/out. Consumers\n * inject it `{ optional: true }` — absent, auth-sensitive data (the program-units menu) is\n * fetched once and never re-fetched.\n *\n * The signal's VALUE is deliberately opaque (`unknown`): consumers only track it for change,\n * never read it — what \"the user\" looks like belongs to the auth package.\n */\nexport const SPARK_AUTH_STATE = new InjectionToken<Signal<unknown>>('SPARK_AUTH_STATE');\n","import { Provider } from '@angular/core';\nimport { SPARK_CONFIG, SparkConfig, defaultSparkConfig } from './spark-config';\n\nexport function provideSpark(config?: Partial<SparkConfig>): Provider[] {\n return [\n {\n provide: SPARK_CONFIG,\n useValue: { ...defaultSparkConfig, ...config }\n }\n ];\n}\n"],"mappings":";AAgBA,MAAa,eAAe,IAAI,eAA4B,cAAc;AAE1E,MAAa,qBAAkC;CAC7C,SAAS;CACT,gBAAgB,EAAE,eAAe,MAAK;AACvC;ACND,MAAa,mBAAmB,IAAI,eAAgC,kBAAkB;ACZtF,SAAgB,aAAa,QAA6B;CACxD,OAAO,CACL;EACE,SAAS;EACT,UAAU;GAAE,GAAG;GAAoB,GAAG;EAAM;CAC7C,CACF;AACH"}
1
+ {"version":3,"file":"mintplayer-ng-spark.mjs","names":[],"sources":["../../src/lib/spark-config.ts","../../src/lib/spark-auth-state.ts","../../src/lib/provide-spark.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\n\nexport interface SparkConfig {\n baseUrl: string;\n /**\n * The concurrency-conflict notice and dialog on the edit page.\n *\n * `showChangedBy`: when true, \"Changed by X\" names whoever wrote the version merged against, but\n * only when History resolves a display name for exactly that version (a `History/T` holder; one\n * `POST /spark/po/revisions` request). Default false: no History request is made and no user is\n * shown. A raw `ModifiedBy` user id is never shown in either mode. The \"at T\" time part is shown\n * whenever the object carries `ModifiedAt`.\n */\n conflictDialog?: { showChangedBy?: boolean };\n}\n\nexport const SPARK_CONFIG = new InjectionToken<SparkConfig>('SPARK_CONFIG');\n\nexport const defaultSparkConfig: SparkConfig = {\n baseUrl: '/spark',\n conflictDialog: { showChangedBy: false },\n};\n","import { InjectionToken, Signal } from '@angular/core';\n\n/**\n * A signal that changes whenever the authenticated user changes — the bridge that lets ng-spark\n * components react to sign-in/out without importing `@mintplayer/ng-spark/auth/*` (the non-auth\n * entry points must not import the auth ones, on purpose, so an app without auth pulls no auth code).\n *\n * `@mintplayer/ng-spark/auth`'s `provideSparkAuth()` supplies it from `SparkAuthService.user`;\n * an app with its own auth stack provides any signal that changes on sign-in/out. Consumers\n * inject it `{ optional: true }` — absent, auth-sensitive data (the program-units menu) is\n * fetched once and never re-fetched.\n *\n * The signal's VALUE is deliberately opaque (`unknown`): consumers only track it for change,\n * never read it — what \"the user\" looks like belongs to the auth entry points.\n */\nexport const SPARK_AUTH_STATE = new InjectionToken<Signal<unknown>>('SPARK_AUTH_STATE');\n","import { Provider } from '@angular/core';\nimport { SPARK_CONFIG, SparkConfig, defaultSparkConfig } from './spark-config';\n\nexport function provideSpark(config?: Partial<SparkConfig>): Provider[] {\n return [\n {\n provide: SPARK_CONFIG,\n useValue: { ...defaultSparkConfig, ...config }\n }\n ];\n}\n"],"mappings":";AAgBA,MAAa,eAAe,IAAI,eAA4B,cAAc;AAE1E,MAAa,qBAAkC;CAC7C,SAAS;CACT,gBAAgB,EAAE,eAAe,MAAK;AACvC;ACND,MAAa,mBAAmB,IAAI,eAAgC,kBAAkB;ACZtF,SAAgB,aAAa,QAA6B;CACxD,OAAO,CACL;EACE,SAAS;EACT,UAAU;GAAE,GAAG;GAAoB,GAAG;EAAM;CAC7C,CACF;AACH"}