@mintplayer/ng-spark 22.27.0 → 22.28.1
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.
- package/fesm2022/mintplayer-ng-spark-grid.mjs +5 -9
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +5 -2
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +13 -5
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +2 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +38 -13
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +28 -9
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +2 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +0 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +3 -2
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mintplayer-ng-spark-grid.d.ts +3 -14
- package/types/mintplayer-ng-spark-models.d.ts +13 -13
- package/types/mintplayer-ng-spark-moderation.d.ts +8 -4
- package/types/mintplayer-ng-spark-pipes.d.ts +3 -0
- package/types/mintplayer-ng-spark-po-create.d.ts +4 -0
- package/types/mintplayer-ng-spark-po-detail.d.ts +4 -0
- package/types/mintplayer-ng-spark-po-edit.d.ts +16 -0
- package/types/mintplayer-ng-spark-po-form.d.ts +1 -0
- package/types/mintplayer-ng-spark-services.d.ts +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-services.mjs","names":[],"sources":["../../services/src/retry-action.service.ts","../../services/src/spark-language.service.ts","../../services/src/spark-streaming.service.ts","../../services/src/spark.service.ts","../../services/src/spark-built-in-icons.ts","../../services/src/spark-icon-registry.ts","../../services/src/spark-query-actions.service.ts","../../services/src/spark-timezone.interceptor.ts","../../services/src/spark-return-navigation.service.ts"],"sourcesContent":["import { Injectable, signal } from '@angular/core';\nimport { RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\n\n@Injectable({ providedIn: 'root' })\nexport class RetryActionService {\n private resolveRetry: ((result: RetryActionResult) => void) | null = null;\n\n payload = signal<RetryActionPayload | null>(null);\n\n show(payload: RetryActionPayload): Promise<RetryActionResult> {\n this.payload.set(payload);\n return new Promise(resolve => { this.resolveRetry = resolve; });\n }\n\n respond(result: RetryActionResult): void {\n this.payload.set(null);\n this.resolveRetry?.(result);\n this.resolveRetry = null;\n }\n}\n","import { Injectable, inject, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { TranslatedString, currentLanguage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\ninterface CultureConfiguration {\n languages: Record<string, TranslatedString>;\n defaultLanguage: string;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class SparkLanguageService {\n private readonly http = inject(HttpClient);\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly currentLang = signal('en');\n private readonly translationsMap = signal<Record<string, TranslatedString>>({});\n\n readonly language = this.currentLang.asReadonly();\n readonly languages = signal<Record<string, TranslatedString>>({});\n\n constructor() {\n this.loadCulture();\n this.loadTranslations();\n }\n\n private async loadCulture(): Promise<void> {\n const config = await firstValueFrom(this.http.get<CultureConfiguration>(`${this.baseUrl}/culture`));\n this.languages.set(config.languages);\n const saved = localStorage.getItem('spark-lang');\n const lang = saved ?? config.defaultLanguage;\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n }\n\n private async loadTranslations(): Promise<void> {\n const t = await firstValueFrom(this.http.get<Record<string, TranslatedString>>(`${this.baseUrl}/translations`));\n this.translationsMap.set(t);\n }\n\n setLanguage(lang: string) {\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n localStorage.setItem('spark-lang', lang);\n }\n\n resolve(ts: TranslatedString | undefined): string {\n if (!ts) return '';\n const lang = this.currentLang();\n return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n }\n\n t(key: string): string {\n const ts = this.translationsMap()[key];\n return this.resolve(ts) || key;\n }\n}\n","import { inject, Injectable, NgZone } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { StreamingMessage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n@Injectable({ providedIn: 'root' })\nexport class SparkStreamingService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly ngZone = inject(NgZone);\n\n connectToStreamingQuery(queryId: string): Observable<StreamingMessage> {\n return new Observable<StreamingMessage>(subscriber => {\n let ws: WebSocket | null = null;\n let retryCount = 0;\n let retryTimeout: ReturnType<typeof setTimeout> | null = null;\n let closed = false;\n const maxRetries = 10;\n const maxDelay = 30000;\n\n const connect = () => {\n if (closed) return;\n\n const url = this.buildWebSocketUrl(queryId);\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n retryCount = 0; // Reset on successful connect\n };\n\n ws.onmessage = (event) => {\n try {\n const message: StreamingMessage = JSON.parse(event.data);\n this.ngZone.run(() => subscriber.next(message));\n } catch {\n // Ignore malformed messages\n }\n };\n\n ws.onerror = () => {\n // Error will trigger onclose\n };\n\n ws.onclose = (event) => {\n if (closed) return;\n\n // Don't reconnect on normal closure\n if (event.code === 1000) {\n this.ngZone.run(() => subscriber.complete());\n return;\n }\n\n // Reconnect with exponential backoff\n if (retryCount < maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, retryCount), maxDelay);\n retryCount++;\n retryTimeout = setTimeout(() => connect(), delay);\n } else {\n this.ngZone.run(() => subscriber.error(new Error('WebSocket connection failed after maximum retries')));\n }\n };\n };\n\n connect();\n\n // Teardown: close WebSocket on unsubscribe\n return () => {\n closed = true;\n if (retryTimeout) {\n clearTimeout(retryTimeout);\n }\n if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {\n ws.close(1000, 'Client unsubscribed');\n }\n };\n });\n }\n\n private buildWebSocketUrl(queryId: string): string {\n const encodedId = encodeURIComponent(queryId);\n const path = `${this.baseUrl}/queries/${encodedId}/stream`;\n\n // If baseUrl is absolute (starts with http/https), replace protocol\n if (this.baseUrl.startsWith('http://')) {\n return path.replace(/^http:\\/\\//, 'ws://');\n }\n if (this.baseUrl.startsWith('https://')) {\n return path.replace(/^https:\\/\\//, 'wss://');\n }\n\n // Relative URL — construct from window.location\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${window.location.host}${path}`;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { HttpClient, HttpErrorResponse } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { CustomActionDefinition, DistinctValuesResult, EntityPermissions, EntityType, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, ProgramUnitsConfiguration, QueryColumnFilter, QueryResult, SparkDeletedFilter, SparkQuery, RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\nimport { ClientOperationEnvelope, RetryOperation, SparkClientOperationDispatcher } from '@mintplayer/ng-spark/client-operations';\nimport { SortColumn } from '@mintplayer/pagination';\nimport { RetryActionService } from './retry-action.service';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n/**\n * Context for {@link SparkService.newObject}. Everything is optional: a standalone New sends an\n * empty body, and an AsDetail row on a parent that has never been saved sends no `parentId` —\n * which is the honest answer, not a gap. The server hands the hook a null parent rather than\n * trusting the client's copy of one.\n */\nexport interface NewObjectOptions {\n /** Name of the parent's AsDetail attribute the row is for; absent for a standalone New. */\n asDetailAttribute?: string;\n /** The parent's entity type — required whenever `asDetailAttribute` is set. */\n parentType?: string;\n /** The parent's id, absent while the parent is itself unsaved. */\n parentId?: string;\n /** Free-form arguments, e.g. which variant a New menu chose. */\n parameters?: Record<string, string>;\n /**\n * The sub-query New was started from (#460, D19), with `parentType` and `parentId` and no\n * `asDetailAttribute`. The server loads the parent, checks the parent's type declares this\n * sub-query, and runs `OnNewAsync` with it — whose base fills the reference to the parent.\n */\n queryId?: string;\n}\n\n/** Where a bulk delete was started from; see {@link SparkService.deleteMany}. */\nexport interface DeleteManyOptions {\n /**\n * The query the rows were selected in. Required (#467, D12): the server fetches the rows through it\n * and applies its `OnDisableActionsAsync` decision; without it the call is a 400.\n */\n queryId: string;\n /** The sub-query's container, when deleting from a sub-query. */\n parentId?: string;\n parentType?: string;\n /** One reason for the whole batch, recorded on every soft-deleted row (#467, D20). */\n reason?: string;\n}\n\n/** A row and the version of it the caller saw (#467, D14): what a bulk delete names. */\nexport interface SparkRowVersion {\n id: string;\n /** The row's `etag` from the query result. */\n etag: string;\n}\n\n/** Context for {@link SparkService.deleteRow}. Every field is required — see `DeleteRow.cs`. */\nexport interface DeleteRowOptions {\n asDetailAttribute: string;\n parentType: string;\n parentId: string;\n /** The row's `__sparkRowKey`, as it arrived from the server. */\n rowKey: string;\n parameters?: Record<string, string>;\n}\n\n/**\n * The union of every body the envelope helpers send.\n *\n * Named rather than inlined per helper because the New and delete-row calls carry neither a\n * `persistentObject` nor a `triggeredBy`: an inline type that did not list their fields would have\n * rejected them at the call site, and spreading them through `any` would have hidden a typo in a\n * field name the server matches by exact spelling.\n */\ntype EnvelopeRequestBody = {\n /** The entity type the request is about — the request parameter, not the submitted object's claim. */\n objectTypeId?: string;\n /** The target object's id, for update and delete. */\n id?: string;\n persistentObject?: any;\n triggeredBy?: string;\n retryResults?: RetryActionResult[];\n /** The version being deleted, for a single delete. */\n etag?: string;\n /** The rows of a bulk delete, each with its version. */\n items?: SparkRowVersion[];\n} & Partial<NewObjectOptions> & Partial<DeleteManyOptions> & Partial<Omit<DeleteRowOptions, 'asDetailAttribute' | 'parentType' | 'parentId'>>;\n\n@Injectable({ providedIn: 'root' })\nexport class SparkService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly http = inject(HttpClient);\n private readonly retryActionService = inject(RetryActionService);\n\n /**\n * How many prompts one request will answer before giving up. Matches the .NET client's\n * `SparkClient.MaxRetryDepth`, and for the same reason: this is a termination bound, not a\n * capacity one. Far above any real conversation (Fleet's longest is two) and far below anything\n * that could hide a spin.\n */\n private static readonly MAX_RETRY_DEPTH = 16;\n private readonly dispatcher = inject(SparkClientOperationDispatcher);\n\n // Entity Types\n async getEntityTypes(): Promise<EntityType[]> {\n return firstValueFrom(this.http.get<EntityType[]>(`${this.baseUrl}/types`));\n }\n\n async getEntityType(id: string): Promise<EntityType> {\n return firstValueFrom(this.http.get<EntityType>(`${this.baseUrl}/types/${encodeURIComponent(id)}`));\n }\n\n async getEntityTypeByClrType(clrType: string): Promise<EntityType | undefined> {\n const types = await this.getEntityTypes();\n return types.find(t => t.clrType === clrType);\n }\n\n // Permissions\n async getPermissions(entityTypeId: string): Promise<EntityPermissions> {\n return firstValueFrom(this.http.get<EntityPermissions>(`${this.baseUrl}/permissions/${encodeURIComponent(entityTypeId)}`));\n }\n\n /** Resolved at most once; see getQueryByName. */\n #queryCatalogue?: Promise<SparkQuery[]>;\n\n // Queries\n async getQueries(): Promise<SparkQuery[]> {\n return firstValueFrom(this.http.get<SparkQuery[]>(`${this.baseUrl}/queries`));\n }\n\n async getQuery(id: string): Promise<SparkQuery> {\n return firstValueFrom(this.http.post<SparkQuery>(`${this.baseUrl}/queries/get`, { queryId: id }));\n }\n\n /**\n * Resolves a query by name, fetching the catalogue at most once per service instance.\n *\n * Without the cache this costs a full `GET /spark/queries` per call, and a form with several\n * reference attributes resolves them in parallel — so opening one page fetched the whole query\n * catalogue N times over. The catalogue is per-caller and effectively static for a page's\n * lifetime; the service is scoped to the app, so a permission change lands on the next load.\n */\n async getQueryByName(name: string): Promise<SparkQuery | undefined> {\n this.#queryCatalogue ??= this.getQueries();\n const queries = await this.#queryCatalogue;\n return queries.find(q => q.name === name);\n }\n\n async executeQuery(queryId: string, options?: ExecuteQueryOptions): Promise<QueryResult> {\n // A POST with a typed body, not a GET with a query string. `sortColumns` used to be encoded as\n // `prop:asc,other:desc` — an encoding invented because a query string has no arrays. Column\n // filtering (several columns, several selected values each) would need a second such encoding,\n // which is the reason the reads moved to POST at all.\n return this.sendRead<QueryResult>(`${this.baseUrl}/queries/execute`, {\n queryId,\n sortColumns: options?.sortColumns?.length\n ? options.sortColumns.map(c => ({ property: c.property, direction: c.direction === 'descending' ? 'desc' : 'asc' }))\n : undefined,\n parentId: options?.parentId,\n parentType: options?.parentType,\n skip: options?.skip,\n take: options?.take,\n search: options?.search,\n // Per-column value filters (#431). The shape that could not be expressed as a query string,\n // and the reason these reads are a POST at all.\n columns: options?.columns?.length ? options.columns : undefined,\n // Soft-deletion mode (#460, T2). Omitted = exclude.\n deleted: options?.deleted,\n // The parent's own mode (#460): a deleted parent opened from the recycle bin. Omitted = exclude.\n parentDeleted: options?.parentDeleted,\n });\n }\n\n /**\n * The distinct values behind one column's filter panel (#431).\n *\n * `columns` carries the OTHER columns' current filters, so the values returned are the ones still\n * reachable — offering a value that empties the grid the moment it is picked is worse than\n * omitting it.\n *\n * An empty result means either \"nothing matches\" or \"you may not enumerate this column\". The two\n * are deliberately indistinguishable, so there is nothing for a caller to branch on.\n */\n async getDistinctValues(queryId: string, column: string, options?: {\n /** Narrows the listed values themselves (the panel's own box). */\n search?: string;\n /**\n * The grid's search term, what `executeQuery` sends as `search` (#460 M15): the values come only\n * from the rows it matches, so a searched grid's panel lists what the grid shows.\n */\n querySearch?: string;\n columns?: QueryColumnFilter[];\n parentId?: string;\n parentType?: string;\n /** The same soft-deletion mode as the grid the panel belongs to (#460). */\n deleted?: SparkDeletedFilter;\n /** The same parent mode as the grid the panel belongs to (#460). */\n parentDeleted?: SparkDeletedFilter;\n }): Promise<DistinctValuesResult> {\n return this.sendRead<DistinctValuesResult>(`${this.baseUrl}/queries/distinct-values`, {\n queryId,\n column,\n search: options?.search || undefined,\n querySearch: options?.querySearch || undefined,\n columns: options?.columns?.length ? options.columns : undefined,\n deleted: options?.deleted,\n parentId: options?.parentId,\n parentType: options?.parentType,\n parentDeleted: options?.parentDeleted,\n });\n }\n\n /**\n * Runs a query identified by name.\n *\n * It used to forward only the parent, dropping skip/take/search — so every reference picker and\n * AsDetail reference column silently saw the server's default of 50 candidates, with no indication\n * that the list was cut. A picker that omits the option you are looking for, and says nothing, is\n * worse than one that fails.\n */\n async executeQueryByName(queryName: string, options?: ExecuteQueryOptions): Promise<QueryResult> {\n const query = await this.getQueryByName(queryName);\n if (!query) return { columns: [], items: [], totalItems: 0, skip: 0, take: options?.take ?? 50 };\n return this.executeQuery(query.id, options);\n }\n\n // Program Units\n async getProgramUnits(): Promise<ProgramUnitsConfiguration> {\n return firstValueFrom(this.http.get<ProgramUnitsConfiguration>(`${this.baseUrl}/program-units`));\n }\n\n // Persistent Objects\n /**\n * Loads one object. `options.deleted` (#460) lets a `ViewDeleted` holder open a soft-deleted row\n * from the recycle bin (`'include'` or `'only'`); without the right the server ignores it and a\n * deleted row stays a 404. Omitted = exclude.\n */\n async get(type: string, id: string, options?: { deleted?: SparkDeletedFilter }): Promise<PersistentObject> {\n return this.sendRead<PersistentObject>(`${this.baseUrl}/po/load`, { objectTypeId: type, id, deleted: options?.deleted });\n }\n\n /**\n * `parent` is the sub-query the New was started from — the same `parentId`/`parentType`/`queryId`\n * `newObject` was given. The server resolves and authorizes it again and hands it to the save hooks\n * as `PersistentObject.Parent`.\n *\n * Resolves to `null` when a server interceptor cancelled the create (#482: the user answered a prompt with\n * Cancel) — the server answers 204 and nothing was created.\n */\n async create(\n type: string,\n data: Partial<PersistentObject>,\n parent?: { parentId: string; parentType: string; queryId: string },\n ): Promise<PersistentObject | null> {\n const created = await this.postWithEnvelope<PersistentObject | null>(\n `${this.baseUrl}/po/create`,\n { objectTypeId: type, persistentObject: data, ...(parent ?? {}) }\n );\n return created ?? null;\n }\n\n /**\n * Saves an edit. `data.etag` — the etag the object was loaded with — is required (#467, D16): it is\n * the version being edited. A 409 means the object changed since (`reason: 'changed'`) or was\n * deleted since (`reason: 'deleted'`).\n */\n async update(type: string, id: string, data: Partial<PersistentObject>): Promise<PersistentObject> {\n if (!data.etag) throw new Error(`An update of '${id}' must carry the etag it was loaded with.`);\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/update`,\n { objectTypeId: type, id, persistentObject: data }\n );\n }\n\n /**\n * Asks the server to reshape an in-progress object after `triggeredBy`'s value changed.\n *\n * Writes nothing, but goes through the envelope like every other mutating call: a refresh may\n * legitimately emit notifications, and may open the retry-action prompt.\n *\n * `triggeredBy` is the attribute's name. For a trigger inside an AsDetail row it is the same\n * path form the inline validation errors use — `Jobs[2].ProfessionId`.\n */\n async refresh(type: string, data: Partial<PersistentObject>, triggeredBy: string): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/refresh`,\n { objectTypeId: type, persistentObject: data, triggeredBy }\n );\n }\n\n /**\n * Asks the server to construct a new object of `type`, so `OnNewAsync` can default it.\n *\n * Writes nothing — the object comes back unsaved, and for an AsDetail row the parent still owns\n * the save. Called by the create page for every New (#460, D19: with `parentId`/`parentType`/\n * `queryId` when started from a sub-query, whose base hook fills the parent reference), and for an\n * AsDetail row only when its type's `serverSideRowLifecycle` is on; every other row type keeps\n * building its blank row locally, which is why switching the flag off costs no request.\n *\n * ⚠️ The row comes back **keyed**: the server constructs the CLR instance, so the row-key field\n * initializer runs and `po.id` carries the key. Flattening it with `nestedPoToDict` therefore\n * populates `__sparkRowKey`, and the row is matchable on save from the moment it is added — the\n * whole reason this round-trip is worth a request.\n */\n async newObject(type: string, options?: NewObjectOptions): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/new`,\n { objectTypeId: type, ...(options ?? {}) }\n );\n }\n\n /**\n * Asks whether a stored row may leave its parent's AsDetail collection.\n *\n * Deletes nothing: a resolved promise means the caller may splice the row out of the collection\n * it is editing, and the removal is persisted with the parent. A hook that refuses rejects with a\n * 400 whose `error.error.result.errors` carries the readable reason, exactly like a save.\n *\n * A POST — as everything is now. It used to be worth explaining: the framework's real delete route\n * was a catch-all that would have swallowed a sibling DELETE. The catch-all is gone with the rest\n * of the route variables.\n */\n async deleteRow(type: string, options: DeleteRowOptions): Promise<void> {\n await this.postWithEnvelope<{ removed: boolean }>(\n `${this.baseUrl}/po/delete-row`,\n { objectTypeId: type, ...options }\n );\n }\n\n /**\n * Deletes one object. `etag` is the version being deleted — the loaded object's `etag` — and is\n * required (#467, D14): an object changed since is a 409.\n */\n async delete(type: string, id: string, etag: string): Promise<void> {\n return this.postWithEnvelope<void>(\n `${this.baseUrl}/po/delete`,\n { objectTypeId: type, id, etag }\n );\n }\n\n /**\n * Deletes several rows of one type in one request — the default Delete action on a selection\n * (`POST /spark/po/delete-many`, #460 D18). Each row carries the `etag` its query result row had\n * (#467, D14). All or nothing: a 404 means some row is missing or not yours to delete, a 403 that\n * the server's `OnDisableActionsAsync` withholds Delete on the query or on one of the rows, a 409\n * that rows changed since the list loaded (the body's `message` names them), a 400 that the\n * selection breaks the rule or the 200-row cap.\n */\n async deleteMany(type: string, items: SparkRowVersion[], options: DeleteManyOptions): Promise<void> {\n return this.postWithEnvelope<void>(\n `${this.baseUrl}/po/delete-many`,\n { objectTypeId: type, items, ...options }\n );\n }\n\n // Custom Actions\n async getCustomActions(objectTypeId: string): Promise<CustomActionDefinition[]> {\n return firstValueFrom(this.http.post<CustomActionDefinition[]>(`${this.baseUrl}/actions/list`, { objectTypeId }));\n }\n\n /**\n * @param parent The object of THIS type the action is operating on — the detail-page invocation.\n * @param selectedItemIds Row ids from a query. Ids, never row objects: a row is a projection.\n * @param queryParent When invoked from a sub-query, the object whose detail page it was rendered\n * on — a DIFFERENT type (the cars listed on a company's page are Cars, the page is a Company).\n * Sent as id + type, exactly as the query endpoint names it, and resolved server-side under its\n * own type with its own Read gate.\n * @param queryId The query the selection came from, so the server can re-run it narrowed to those\n * ids and hand the action the rows the grid actually had -- index-computed columns included.\n * @returns What the action handed to `CustomActionArgs.SetResult` (#460, T5) -- the envelope's\n * `result` -- or `undefined` when it set nothing. The value is the action author's to shape: it\n * bypasses the server's redaction. A 403 means the action is disabled for this object, query or\n * selection (the server's `OnDisableActionsAsync` said so), distinct from a 404 for a row the\n * caller cannot see.\n */\n async executeCustomAction<T = unknown>(\n objectTypeId: string,\n actionName: string,\n parent?: PersistentObject,\n selectedItemIds?: string[],\n queryParent?: { id: string; type: string },\n queryId?: string,\n ): Promise<T | undefined> {\n const body: {\n objectTypeId: string; actionName: string;\n parent?: PersistentObject; selectedItemIds?: string[];\n parentId?: string; parentType?: string; queryId?: string;\n retryResults?: RetryActionResult[];\n } = { objectTypeId, actionName, parent, selectedItemIds, parentId: queryParent?.id, parentType: queryParent?.type, queryId };\n // A literal `null` result (the action set nothing) becomes undefined, so callers test one thing.\n const result = await this.postWithEnvelope<T | null>(\n `${this.baseUrl}/actions/execute`,\n body as any\n );\n return result ?? undefined;\n }\n\n // LookupReferences\n async getLookupReferences(): Promise<LookupReferenceListItem[]> {\n return firstValueFrom(this.http.get<LookupReferenceListItem[]>(`${this.baseUrl}/lookupref`));\n }\n\n async getLookupReference(name: string): Promise<LookupReference> {\n return firstValueFrom(this.http.get<LookupReference>(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`));\n }\n\n async addLookupReferenceValue(name: string, value: LookupReferenceValue): Promise<LookupReferenceValue> {\n return firstValueFrom(this.http.post<LookupReferenceValue>(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`, value));\n }\n\n async updateLookupReferenceValue(name: string, key: string, value: LookupReferenceValue): Promise<LookupReferenceValue> {\n return firstValueFrom(this.http.put<LookupReferenceValue>(\n `${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`,\n value\n ));\n }\n\n async deleteLookupReferenceValue(name: string, key: string): Promise<void> {\n return firstValueFrom(this.http.delete<void>(\n `${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`\n ));\n }\n\n /**\n * Posts to an add-on package's endpoint under the Spark base URL (`/spark` + `path`) and unwraps\n * the `ClientOperationEnvelope` exactly like the built-in mutations: operations are dispatched and\n * a 449 retry opens the retry modal. For the ng-spark add-on entry points (`soft-delete`,\n * `history`, `moderation`) whose server packages answer through core's envelope helpers\n * (`SparkAddOnEndpoints`, #460).\n */\n postEnvelope<T>(path: string, body: Record<string, unknown>): Promise<T> {\n return this.postWithEnvelope<T>(`${this.baseUrl}${path}`, body as EnvelopeRequestBody);\n }\n\n // Envelope-aware HTTP helpers.\n // All mutation endpoints (Create / Update / Delete / Execute custom action) emit the\n // ClientOperationEnvelope { result, operations } shape. These helpers unwrap the envelope,\n // dispatch any non-retry operations, and translate 449 retry-operations into the existing\n // RetryActionService modal flow.\n\n private postWithEnvelope<T>(url: string, body: EnvelopeRequestBody): Promise<T> {\n return this.sendWithEnvelope<T>(\n () => firstValueFrom(this.http.post<ClientOperationEnvelope<T>>(url, body)),\n body,\n () => this.postWithEnvelope<T>(url, body),\n );\n }\n\n /**\n * A read: posts the request, and returns the response **as-is** rather than unwrapping an\n * envelope.\n *\n * The read endpoints moved from GET to POST so their hooks could prompt, but their success\n * responses are still bare — a `PersistentObject`, a `QueryResult`. Only the 449 is enveloped,\n * because a retry has nowhere else to live. So this shares the retry loop with the mutating calls\n * and nothing else.\n */\n private async sendRead<T>(url: string, body: Record<string, unknown>): Promise<T> {\n try {\n return await firstValueFrom(this.http.post<T>(url, body));\n } catch (error) {\n return this.handleEnvelopeRetryError<T>(\n error as HttpErrorResponse,\n () => this.sendRead<T>(url, body),\n body as { retryResults?: RetryActionResult[] },\n );\n }\n }\n\n private async sendWithEnvelope<T>(\n send: () => Promise<ClientOperationEnvelope<T>>,\n body: { retryResults?: RetryActionResult[] },\n retryFn: () => Promise<T>,\n ): Promise<T> {\n try {\n const envelope = await send();\n if (envelope?.operations?.length) {\n this.dispatcher.dispatch(envelope.operations);\n }\n return envelope?.result as T;\n } catch (error) {\n return this.handleEnvelopeRetryError<T>(error as HttpErrorResponse, retryFn, body);\n }\n }\n\n private async handleEnvelopeRetryError<T>(\n error: HttpErrorResponse,\n retryFn: () => Promise<T>,\n body: { retryResults?: RetryActionResult[] }\n ): Promise<T> {\n if (error.status !== 449) throw error;\n const envelope = error.error as ClientOperationEnvelope<T> | undefined;\n if (!envelope?.operations?.length) throw error;\n\n // Dispatch any non-retry operations accumulated before the retry throw\n // so notify/refresh/etc. fire BEFORE the retry modal opens.\n const nonRetry = envelope.operations.filter(o => o.type !== 'retry');\n if (nonRetry.length) this.dispatcher.dispatch(nonRetry);\n\n const retryOp = envelope.operations.find(o => o.type === 'retry') as RetryOperation | undefined;\n if (!retryOp) throw error;\n\n // ⚠️ A hook that raises a retry WITHOUT first checking `Retry.Result` re-raises on every\n // resubmission, and this loop has no natural end: the modal reopens, the user answers, the\n // server asks again. Each round trip looks individually reasonable, so the tab spins until it is\n // closed and nothing is logged. The trap is documented in RetryFromEveryHookTests, and the .NET\n // client has had a bound since M3; this is the browser's.\n //\n // The bound is on ANSWERS ALREADY GIVEN, which is what `retryResults` is — so it counts the\n // conversation and not the component's lifetime, and two unrelated conversations do not add up.\n if ((body.retryResults?.length ?? 0) >= SparkService.MAX_RETRY_DEPTH) {\n throw new Error(\n `Gave up after answering ${SparkService.MAX_RETRY_DEPTH} retry prompts; the last was step ` +\n `${retryOp.step} (\"${retryOp.title}\"). A hook that raises a retry without checking ` +\n `Retry.Result first will do this — it re-raises on every resubmission.`);\n }\n\n const payload: RetryActionPayload = {\n type: 'retry-action',\n step: retryOp.step,\n title: retryOp.title,\n options: retryOp.options,\n defaultOption: retryOp.defaultOption ?? undefined,\n persistentObject: retryOp.persistentObject ?? undefined,\n message: retryOp.message ?? undefined,\n };\n const result = await this.retryActionService.show(payload);\n if (result.option === 'Cancel' && !payload.options.includes('Cancel')) throw error;\n\n body.retryResults = [...(body.retryResults || []), result];\n return retryFn();\n }\n}\n\n/**\n * The options every query execution takes.\n *\n * Extracted because `executeQueryByName` forwards to `executeQuery`, and while both spelled their\n * options as separate inline literals a field added to one silently failed to forward from the\n * other — which is how skip/take/search were once dropped for every reference picker.\n */\nexport interface ExecuteQueryOptions {\n sortColumns?: SortColumn[];\n parentId?: string;\n parentType?: string;\n skip?: number;\n take?: number;\n search?: string;\n /** Per-column value filters (#431). Columns AND together; values within a column OR. */\n columns?: QueryColumnFilter[];\n /**\n * Soft-deletion mode (#460): `exclude` (default), `include`, `only`. Sent only when set; the server\n * honours a widening only for callers holding `ViewDeleted` on the type.\n */\n deleted?: SparkDeletedFilter;\n /**\n * The mode the PARENT (`parentId`/`parentType`) is resolved under (#460) — `include` for a sub-query\n * on a deleted object's page, opened from the recycle bin. Independent of `deleted`, which stays the\n * rows' filter. Honoured only for callers holding `ViewDeleted` on the parent's type; anyone else\n * gets the same 404 a missing parent gives.\n */\n parentDeleted?: SparkDeletedFilter;\n}\n","/** Built-in SVG icons used by ng-spark library templates. */\nexport const SPARK_BUILT_IN_ICONS: Record<string, string> = {\n 'three-dots-vertical': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-three-dots-vertical\" viewBox=\"0 0 16 16\"><path d=\"M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0\"/></svg>',\n 'x-circle': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-circle\" viewBox=\"0 0 16 16\"><path d=\"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16\"/><path d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708\"/></svg>',\n 'arrow-left': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-left\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8\"/></svg>',\n 'pencil': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-pencil\" viewBox=\"0 0 16 16\"><path d=\"M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325\"/></svg>',\n 'plus': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus\" viewBox=\"0 0 16 16\"><path d=\"M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4\"/></svg>',\n 'plus-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus-lg\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2\"/></svg>',\n 'search': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-search\" viewBox=\"0 0 16 16\"><path d=\"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0\"/></svg>',\n 'trash': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-trash\" viewBox=\"0 0 16 16\"><path d=\"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z\"/><path d=\"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z\"/></svg>',\n 'x-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-lg\" viewBox=\"0 0 16 16\"><path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z\"/></svg>',\n};\n","import { Injectable, inject } from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { SPARK_BUILT_IN_ICONS } from './spark-built-in-icons';\n\n/**\n * R2-M12: SVG icon registry. The registry no longer accepts a raw string —\n * apps must pre-sanitize via `DomSanitizer.bypassSecurityTrustHtml` (or\n * `parseSvg` to validate) before calling `register`. Treating the parameter\n * as `SafeHtml` puts the explicit bypass decision in the caller's hands so a\n * developer wiring server-supplied SVG (per-tenant branding fetched from a\n * JSON endpoint) doesn't accidentally trust a `<svg><script>...</script>`\n * payload.\n *\n * The built-in icons baked into the package are still bypass-trusted internally\n * because they're known-safe at build time.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkIconRegistry {\n private sanitizer = inject(DomSanitizer);\n private icons = new Map<string, SafeHtml>();\n\n constructor() {\n for (const [name, svg] of Object.entries(SPARK_BUILT_IN_ICONS)) {\n this.icons.set(name, this.sanitizer.bypassSecurityTrustHtml(svg));\n }\n }\n\n /**\n * Registers a pre-sanitized SVG icon. Callers MUST validate or explicitly\n * trust the SVG before passing — for example:\n *\n * ```ts\n * const safe = sanitizer.bypassSecurityTrustHtml(svgFromBuildTimeConstant);\n * registry.register('app-logo', safe);\n * ```\n *\n * For server-supplied SVG, prefer a strict allow-list parser (no `<script>`,\n * no `on*` attributes, no `href=\"javascript:...\"`) before trusting.\n */\n register(name: string, svg: SafeHtml): void {\n this.icons.set(name, svg);\n }\n\n get(name: string): SafeHtml | undefined {\n return this.icons.get(name);\n }\n\n has(name: string): boolean {\n return this.icons.has(name);\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport { CustomActionDefinition, PersistentObject, SparkQuery, EntityType, filterQueryActions } from '@mintplayer/ng-spark/models';\nimport { SparkService } from './spark.service';\n\n/**\n * A query's custom actions, reachable **without the grid that usually renders them**.\n *\n * ## Why this exists\n *\n * A query's actions were only ever obtainable by rendering `<spark-query-grid>`, which loads the\n * query, resolves its entity type, fetches the actions and filters them to the ones marked for a\n * query surface — all privately. A page that wanted the same buttons somewhere else (a toolbar\n * above the card, the shell's topbar) had three bad options: duplicate the four-step resolution\n * and let it drift, reach into the grid's internals, or put the button in the wrong place.\n *\n * This exposes the resolution itself. The grid keeps rendering the default placement; a host that\n * wants a different one asks here and renders its own control.\n *\n * ## What it does not do\n *\n * It does not authorize. `/spark/actions/{type}` returns only the actions this caller may see —\n * the server filters against `security.json`, and executing re-checks — so nothing here is a gate,\n * and a host must not treat \"the list came back empty\" as anything other than a display fact.\n *\n * It also does not cache. Actions depend on the caller, and a memo keyed by type id would survive\n * a sign-out.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkQueryActionsService {\n private readonly sparkService = inject(SparkService);\n\n /**\n * The custom actions of the query named by id or alias, already filtered to those that belong on\n * a query surface.\n *\n * Returns an empty list — rather than throwing — when the query resolves to no entity type,\n * because a caller rendering a toolbar wants \"no buttons\", not a broken page.\n */\n async actionsFor(queryIdOrAlias: string): Promise<CustomActionDefinition[]> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) return [];\n\n // filterQueryActions, not a local predicate: the grid uses it too, and the `showedOn` values\n // are exactly the kind of thing a second copy gets subtly wrong — both grids once tested for\n // \"list\", a value nothing emits, and every correctly authored action rendered nowhere.\n return filterQueryActions(await this.sparkService.getCustomActions(context.entityType.id));\n }\n\n /**\n * Runs one of those actions. `selectedItemIds` are row ids, exactly as the grid posts them —\n * the server re-materializes each one through the same load path a detail page uses, so an id\n * from anywhere is treated as caller input rather than as a verified row.\n */\n async execute(\n queryIdOrAlias: string,\n actionName: string,\n options?: {\n parent?: PersistentObject;\n selectedItemIds?: string[];\n /** For a sub-query: the object whose detail page it was rendered on. See SparkService. */\n queryParent?: { id: string; type: string };\n },\n ): Promise<void> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) {\n throw new Error(\n `Cannot execute '${actionName}': query '${queryIdOrAlias}' resolves to no entity type, so there ` +\n `is nothing to execute it against.`);\n }\n\n await this.sparkService.executeCustomAction(\n context.entityType.id, actionName, options?.parent, options?.selectedItemIds, options?.queryParent,\n context.query.id);\n }\n\n /**\n * The query and the entity type its rows are mapped against — the two-step resolution both\n * methods need, kept in one place so they cannot disagree about which type an action runs on.\n */\n private async contextFor(\n queryIdOrAlias: string,\n ): Promise<{ query: SparkQuery; entityType: EntityType } | null> {\n const [query, entityTypes] = await Promise.all([\n this.sparkService.getQuery(queryIdOrAlias),\n this.sparkService.getEntityTypes(),\n ]);\n if (!query) return null;\n\n const entityType = entityTypes.find(t => t.name === query.entityType)\n ?? entityTypes.find(t => t.id === query.entityType);\n return entityType ? { query, entityType } : null;\n }\n}\n","import { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { HttpFeature, HttpFeatureKind, HttpInterceptorFn, withInterceptors } from '@angular/common/http';\nimport { PLATFORM_ID, inject } from '@angular/core';\n\n/**\n * The header carrying the viewer's IANA timezone id, e.g. `Europe/Brussels`.\n * Must stay in lockstep with `RequestTimeZoneResolver.HeaderName` on the server.\n */\nexport const SPARK_TIMEZONE_HEADER = 'X-Spark-Timezone';\n\n/**\n * The default name of the cookie carrying the same id, for requests the browser makes without any\n * script running first (a server-side render, a link opened from a mail). Must stay in lockstep\n * with `SparkTimeZoneOptions.DefaultCookieName` (`Spark:TimeZone:CookieName`) on the server.\n */\nexport const SPARK_TIMEZONE_COOKIE = 'spark-timezone';\n\n/** Options for {@link withSparkTimezone}. */\nexport interface SparkTimezoneOptions {\n /**\n * The cookie to write the viewer's zone into; `false` writes none (the header is still sent).\n * Defaults to {@link SPARK_TIMEZONE_COOKIE}. Use the same name as the server's\n * `Spark:TimeZone:CookieName`.\n */\n cookieName?: string | false;\n}\n\n/** One year: the zone only changes when the viewer travels, and each visit refreshes it. */\nconst COOKIE_MAX_AGE_SECONDS = 31_536_000;\n\n/** The server's shape check (`RequestTimeZoneResolver`): an id failing it would be ignored anyway. */\nconst ZONE_ID_SHAPE = /^[A-Za-z][A-Za-z0-9_+\\-]*(\\/[A-Za-z0-9_+\\-]+){0,2}$/;\n\n/**\n * Returns the browser's IANA zone id, or `null` when the environment cannot name one.\n *\n * An id is deliberately sent rather than a numeric offset: an offset is only valid at one instant,\n * so a server asked to render \"next Tuesday at 09:00\" from an offset captured today gets it wrong\n * across a DST boundary. A zone id carries the rules, not one sample of them.\n */\nexport function resolveBrowserTimeZone(): string | null {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || null;\n } catch {\n return null;\n }\n}\n\n/** The value of cookie `name` in `cookies` (`document.cookie`), or `null`. */\nfunction readCookie(cookies: string, name: string): string | null {\n for (const part of cookies.split(';')) {\n const eq = part.indexOf('=');\n if (eq > 0 && part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return null;\n}\n\n/**\n * Writes `zone` into cookie `name` unless it already holds it. Browser only; the server never\n * writes this cookie. `Secure` on https, `SameSite=Lax` so a link from a mail still carries it.\n */\nfunction writeTimezoneCookie(doc: Document, name: string, zone: string): void {\n if (readCookie(doc.cookie, name) === zone) return;\n const secure = doc.location?.protocol === 'https:' ? '; Secure' : '';\n doc.cookie = `${name}=${zone}; Path=/; Max-Age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax${secure}`;\n}\n\n/**\n * Builds the interceptor for {@link withSparkTimezone}. Exported for apps that compose their own\n * `withInterceptors([...])`.\n *\n * **This does not participate in reading or writing timestamps.** A `DateTimeOffset` means an\n * instant, and the browser converts in both directions on those paths. The header and cookie exist\n * for work the server does on its own — a scheduled export, a notification email, a server-side\n * render — where there is no browser in the loop to do the converting but the output still has to\n * read as local time.\n *\n * ⚠️ **Nothing happens on the server platform.** During a server-side render `Intl` names the Node\n * process's zone (UTC in a container), and a header carrying it would win over the viewer's cookie\n * on the server (spike S-TZ3, #460).\n */\nexport function createSparkTimezoneInterceptor(options: SparkTimezoneOptions = {}): HttpInterceptorFn {\n const cookieName = options.cookieName === undefined ? SPARK_TIMEZONE_COOKIE : options.cookieName;\n\n return (req, next) => {\n if (!isPlatformBrowser(inject(PLATFORM_ID))) return next(req);\n\n const zone = resolveBrowserTimeZone();\n if (!zone) return next(req);\n\n if (cookieName && ZONE_ID_SHAPE.test(zone)) {\n writeTimezoneCookie(inject(DOCUMENT), cookieName, zone);\n }\n\n // Never annotate a cross-origin call: the viewer's timezone is a (small) fingerprinting signal\n // and third-party hosts have no business receiving it.\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(req.url)) {\n try {\n if (new URL(req.url).origin !== window.location.origin) return next(req);\n } catch {\n return next(req);\n }\n }\n\n return next(req.clone({ setHeaders: { [SPARK_TIMEZONE_HEADER]: zone } }));\n };\n}\n\n/**\n * Attaches {@link SPARK_TIMEZONE_HEADER} to same-origin requests and keeps the\n * {@link SPARK_TIMEZONE_COOKIE} cookie current — with the default options. See\n * {@link createSparkTimezoneInterceptor}.\n */\nexport const sparkTimezoneInterceptor: HttpInterceptorFn = createSparkTimezoneInterceptor();\n\n/**\n * `provideHttpClient(...withSparkTimezone())` — registers the timezone interceptor.\n *\n * Spreadable alongside `withSparkAuth()`:\n * `provideHttpClient(...withSparkAuth(), ...withSparkTimezone())`.\n *\n * The cookie is written on the first request the browser app makes, so the very first server-side\n * render of a first visit has no cookie and renders in UTC.\n */\nexport function withSparkTimezone(options?: SparkTimezoneOptions): HttpFeature<HttpFeatureKind>[] {\n return [withInterceptors([options ? createSparkTimezoneInterceptor(options) : sparkTimezoneInterceptor])];\n}\n","import { inject, Injectable } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { SparkService } from './spark.service';\n\n/** The `history.state` key a list row link stores the list's own URL under. */\nexport const SPARK_RETURN_URL_STATE_KEY = 'sparkReturnUrl';\n\n/**\n * Where a detail page goes back to after its row stops existing — a Delete, or a Purge from the\n * recycle bin (#460).\n *\n * Neither \"the start page\" nor \"the previous history entry\" is the right answer: the first loses the\n * user's place, the second may be the deleted row's edit page, or another site. The right answer is\n * the list the row was opened from, so a list's row link records that list's URL in the navigation\n * state ({@link stateForRowLink}); a detail page reached otherwise (a bookmark, a reference link)\n * falls back to the row type's own list — its program-unit query, else any query over the type.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkReturnNavigationService {\n private readonly router = inject(Router);\n private readonly spark = inject(SparkService);\n\n /** Navigation state for a row link: the current URL (the list, with its `?deleted=` mode). */\n stateForRowLink(): Record<string, string> {\n return { [SPARK_RETURN_URL_STATE_KEY]: this.router.url };\n }\n\n /** The list URL the current detail page was opened from, when a row link recorded one. */\n recordedReturnUrl(): string | null {\n const state = (typeof history !== 'undefined' ? history.state : null) as Record<string, unknown> | null;\n const url = state?.[SPARK_RETURN_URL_STATE_KEY];\n // Only an in-app path: the state is the app's own, but a protocol-relative \"//host\" is not a path.\n return typeof url === 'string' && url.startsWith('/') && !url.startsWith('//') ? url : null;\n }\n\n /**\n * Navigates to the list the row came from, else to the type's list (`?deleted=only` kept when\n * `deletedView`), else to the start page. `replaceUrl`: the gone row's page is not worth a Back.\n */\n async returnToList(entityTypeName: string | null | undefined, options?: { deletedView?: boolean }): Promise<void> {\n const recorded = this.recordedReturnUrl();\n if (recorded) {\n await this.router.navigateByUrl(recorded, { replaceUrl: true });\n return;\n }\n const route = entityTypeName ? await this.listRouteFor(entityTypeName) : null;\n if (route) {\n await this.router.navigate(route, {\n replaceUrl: true,\n ...(options?.deletedView ? { queryParams: { deleted: 'only' } } : {}),\n });\n return;\n }\n await this.router.navigate(['/'], { replaceUrl: true });\n }\n\n /** `['/query', alias]` of the type's list: a menu (program-unit) query first, else any query. */\n async listRouteFor(entityTypeName: string): Promise<unknown[] | null> {\n let queries;\n try {\n queries = (await this.spark.getQueries()).filter(q => q.entityType === entityTypeName);\n } catch {\n return null;\n }\n if (!queries.length) return null;\n try {\n const units = (await this.spark.getProgramUnits()).programUnitGroups\n .flatMap(g => g.programUnits)\n .filter(u => u.type === 'query')\n .sort((a, b) => a.order - b.order);\n for (const unit of units) {\n if (queries.some(q => q.id === unit.queryId || (!!unit.alias && q.alias === unit.alias)))\n return ['/query', unit.alias || unit.queryId!];\n }\n } catch {\n // No menu: any query over the type still beats the start page.\n }\n return ['/query', queries[0].alias || queries[0].id];\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-services.mjs","names":[],"sources":["../../services/src/retry-action.service.ts","../../services/src/spark-language.service.ts","../../services/src/spark-streaming.service.ts","../../services/src/spark.service.ts","../../services/src/spark-built-in-icons.ts","../../services/src/spark-icon-registry.ts","../../services/src/spark-query-actions.service.ts","../../services/src/spark-timezone.interceptor.ts","../../services/src/spark-return-navigation.service.ts"],"sourcesContent":["import { Injectable, signal } from '@angular/core';\nimport { RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\n\n@Injectable({ providedIn: 'root' })\nexport class RetryActionService {\n private resolveRetry: ((result: RetryActionResult) => void) | null = null;\n\n payload = signal<RetryActionPayload | null>(null);\n\n show(payload: RetryActionPayload): Promise<RetryActionResult> {\n this.payload.set(payload);\n return new Promise(resolve => { this.resolveRetry = resolve; });\n }\n\n respond(result: RetryActionResult): void {\n this.payload.set(null);\n this.resolveRetry?.(result);\n this.resolveRetry = null;\n }\n}\n","import { Injectable, inject, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { TranslatedString, currentLanguage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\ninterface CultureConfiguration {\n languages: Record<string, TranslatedString>;\n defaultLanguage: string;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class SparkLanguageService {\n private readonly http = inject(HttpClient);\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly currentLang = signal('en');\n private readonly translationsMap = signal<Record<string, TranslatedString>>({});\n\n readonly language = this.currentLang.asReadonly();\n readonly languages = signal<Record<string, TranslatedString>>({});\n\n constructor() {\n this.loadCulture();\n this.loadTranslations();\n }\n\n private async loadCulture(): Promise<void> {\n const config = await firstValueFrom(this.http.get<CultureConfiguration>(`${this.baseUrl}/culture`));\n this.languages.set(config.languages);\n const saved = localStorage.getItem('spark-lang');\n const lang = saved ?? config.defaultLanguage;\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n }\n\n private async loadTranslations(): Promise<void> {\n const t = await firstValueFrom(this.http.get<Record<string, TranslatedString>>(`${this.baseUrl}/translations`));\n this.translationsMap.set(t);\n }\n\n setLanguage(lang: string) {\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n localStorage.setItem('spark-lang', lang);\n }\n\n resolve(ts: TranslatedString | undefined): string {\n if (!ts) return '';\n const lang = this.currentLang();\n return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n }\n\n t(key: string): string {\n const ts = this.translationsMap()[key];\n return this.resolve(ts) || key;\n }\n}\n","import { inject, Injectable, NgZone } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { StreamingMessage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n@Injectable({ providedIn: 'root' })\nexport class SparkStreamingService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly ngZone = inject(NgZone);\n\n connectToStreamingQuery(queryId: string): Observable<StreamingMessage> {\n return new Observable<StreamingMessage>(subscriber => {\n let ws: WebSocket | null = null;\n let retryCount = 0;\n let retryTimeout: ReturnType<typeof setTimeout> | null = null;\n let closed = false;\n const maxRetries = 10;\n const maxDelay = 30000;\n\n const connect = () => {\n if (closed) return;\n\n const url = this.buildWebSocketUrl(queryId);\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n retryCount = 0; // Reset on successful connect\n };\n\n ws.onmessage = (event) => {\n try {\n const message: StreamingMessage = JSON.parse(event.data);\n this.ngZone.run(() => subscriber.next(message));\n } catch {\n // Ignore malformed messages\n }\n };\n\n ws.onerror = () => {\n // Error will trigger onclose\n };\n\n ws.onclose = (event) => {\n if (closed) return;\n\n // Don't reconnect on normal closure\n if (event.code === 1000) {\n this.ngZone.run(() => subscriber.complete());\n return;\n }\n\n // Reconnect with exponential backoff\n if (retryCount < maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, retryCount), maxDelay);\n retryCount++;\n retryTimeout = setTimeout(() => connect(), delay);\n } else {\n this.ngZone.run(() => subscriber.error(new Error('WebSocket connection failed after maximum retries')));\n }\n };\n };\n\n connect();\n\n // Teardown: close WebSocket on unsubscribe\n return () => {\n closed = true;\n if (retryTimeout) {\n clearTimeout(retryTimeout);\n }\n if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {\n ws.close(1000, 'Client unsubscribed');\n }\n };\n });\n }\n\n private buildWebSocketUrl(queryId: string): string {\n const encodedId = encodeURIComponent(queryId);\n const path = `${this.baseUrl}/queries/${encodedId}/stream`;\n\n // If baseUrl is absolute (starts with http/https), replace protocol\n if (this.baseUrl.startsWith('http://')) {\n return path.replace(/^http:\\/\\//, 'ws://');\n }\n if (this.baseUrl.startsWith('https://')) {\n return path.replace(/^https:\\/\\//, 'wss://');\n }\n\n // Relative URL — construct from window.location\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${window.location.host}${path}`;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { HttpClient, HttpErrorResponse } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { CustomActionDefinition, DistinctValuesResult, EntityPermissions, EntityType, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, ProgramUnitsConfiguration, QueryColumnFilter, QueryResult, SparkDeletedFilter, SparkQuery, RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\nimport { ClientOperationEnvelope, RetryOperation, SparkClientOperationDispatcher } from '@mintplayer/ng-spark/client-operations';\nimport { SortColumn } from '@mintplayer/pagination';\nimport { RetryActionService } from './retry-action.service';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n/**\n * Context for {@link SparkService.newObject}. Everything is optional: a standalone New sends an\n * empty body, and an AsDetail row on a parent that has never been saved sends no `parentId` —\n * which is the honest answer, not a gap. The server hands the hook a null parent rather than\n * trusting the client's copy of one.\n */\nexport interface NewObjectOptions {\n /** Name of the parent's AsDetail attribute the row is for; absent for a standalone New. */\n asDetailAttribute?: string;\n /** The parent's entity type — required whenever `asDetailAttribute` is set. */\n parentType?: string;\n /** The parent's id, absent while the parent is itself unsaved. */\n parentId?: string;\n /** Free-form arguments, e.g. which variant a New menu chose. */\n parameters?: Record<string, string>;\n /**\n * The sub-query New was started from (#460, D19), with `parentType` and `parentId` and no\n * `asDetailAttribute`. The server loads the parent, checks the parent's type declares this\n * sub-query, and runs `OnNewAsync` with it — whose base fills the reference to the parent.\n */\n queryId?: string;\n}\n\n/** Where a bulk delete was started from; see {@link SparkService.deleteMany}. */\nexport interface DeleteManyOptions {\n /**\n * The query the rows were selected in. Required (#467, D12): the server fetches the rows through it\n * and applies its `OnDisableActionsAsync` decision; without it the call is a 400.\n */\n queryId: string;\n /** The sub-query's container, when deleting from a sub-query. */\n parentId?: string;\n parentType?: string;\n /** One reason for the whole batch, recorded on every soft-deleted row (#467, D20). */\n reason?: string;\n}\n\n/** A row and the version of it the caller saw (#467, D14): what a bulk delete names. */\nexport interface SparkRowVersion {\n id: string;\n /** The row's `etag` from the query result. */\n etag: string;\n}\n\n/** Context for {@link SparkService.deleteRow}. Every field is required — see `DeleteRow.cs`. */\nexport interface DeleteRowOptions {\n asDetailAttribute: string;\n parentType: string;\n parentId: string;\n /** The row's `__sparkRowKey`, as it arrived from the server. */\n rowKey: string;\n parameters?: Record<string, string>;\n}\n\n/**\n * The union of every body the envelope helpers send.\n *\n * Named rather than inlined per helper because the New and delete-row calls carry neither a\n * `persistentObject` nor a `triggeredBy`: an inline type that did not list their fields would have\n * rejected them at the call site, and spreading them through `any` would have hidden a typo in a\n * field name the server matches by exact spelling.\n */\ntype EnvelopeRequestBody = {\n /** The entity type the request is about — the request parameter, not the submitted object's claim. */\n objectTypeId?: string;\n /** The target object's id, for update and delete. */\n id?: string;\n persistentObject?: any;\n triggeredBy?: string;\n retryResults?: RetryActionResult[];\n /** The version being deleted, for a single delete. */\n etag?: string;\n /** The rows of a bulk delete, each with its version. */\n items?: SparkRowVersion[];\n} & Partial<NewObjectOptions> & Partial<DeleteManyOptions> & Partial<Omit<DeleteRowOptions, 'asDetailAttribute' | 'parentType' | 'parentId'>>;\n\n@Injectable({ providedIn: 'root' })\nexport class SparkService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly http = inject(HttpClient);\n private readonly retryActionService = inject(RetryActionService);\n\n /**\n * How many prompts one request will answer before giving up. Matches the .NET client's\n * `SparkClient.MaxRetryDepth`, and for the same reason: this is a termination bound, not a\n * capacity one. Far above any real conversation (Fleet's longest is two) and far below anything\n * that could hide a spin.\n */\n private static readonly MAX_RETRY_DEPTH = 16;\n private readonly dispatcher = inject(SparkClientOperationDispatcher);\n\n // Entity Types\n async getEntityTypes(): Promise<EntityType[]> {\n return firstValueFrom(this.http.get<EntityType[]>(`${this.baseUrl}/types`));\n }\n\n /**\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 const payload: RetryActionPayload = {\n type: 'retry-action',\n step: retryOp.step,\n title: retryOp.title,\n options: retryOp.options,\n defaultOption: retryOp.defaultOption ?? undefined,\n persistentObject: retryOp.persistentObject ?? undefined,\n message: retryOp.message ?? undefined,\n };\n const result = await this.retryActionService.show(payload);\n if (result.option === 'Cancel' && !payload.options.includes('Cancel')) throw error;\n\n body.retryResults = [...(body.retryResults || []), result];\n return retryFn();\n }\n}\n\n/**\n * The options every query execution takes.\n *\n * Extracted because `executeQueryByName` forwards to `executeQuery`, and while both spelled their\n * options as separate inline literals a field added to one silently failed to forward from the\n * other — which is how skip/take/search were once dropped for every reference picker.\n */\nexport interface ExecuteQueryOptions {\n sortColumns?: SortColumn[];\n parentId?: string;\n parentType?: string;\n skip?: number;\n take?: number;\n search?: string;\n /** Per-column value filters (#431). Columns AND together; values within a column OR. */\n columns?: QueryColumnFilter[];\n /**\n * Soft-deletion mode (#460): `exclude` (default), `include`, `only`. Sent only when set; the server\n * honours a widening only for callers holding `ViewDeleted` on the type.\n */\n deleted?: SparkDeletedFilter;\n /**\n * The mode the PARENT (`parentId`/`parentType`) is resolved under (#460) — `include` for a sub-query\n * on a deleted object's page, opened from the recycle bin. Independent of `deleted`, which stays the\n * rows' filter. Honoured only for callers holding `ViewDeleted` on the parent's type; anyone else\n * gets the same 404 a missing parent gives.\n */\n parentDeleted?: SparkDeletedFilter;\n}\n","/** Built-in SVG icons used by ng-spark library templates. */\nexport const SPARK_BUILT_IN_ICONS: Record<string, string> = {\n 'three-dots-vertical': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-three-dots-vertical\" viewBox=\"0 0 16 16\"><path d=\"M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0\"/></svg>',\n 'x-circle': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-circle\" viewBox=\"0 0 16 16\"><path d=\"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16\"/><path d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708\"/></svg>',\n 'arrow-left': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-left\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8\"/></svg>',\n 'pencil': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-pencil\" viewBox=\"0 0 16 16\"><path d=\"M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325\"/></svg>',\n 'plus': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus\" viewBox=\"0 0 16 16\"><path d=\"M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4\"/></svg>',\n 'plus-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus-lg\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2\"/></svg>',\n 'search': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-search\" viewBox=\"0 0 16 16\"><path d=\"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0\"/></svg>',\n 'trash': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-trash\" viewBox=\"0 0 16 16\"><path d=\"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z\"/><path d=\"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z\"/></svg>',\n 'x-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-lg\" viewBox=\"0 0 16 16\"><path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z\"/></svg>',\n};\n","import { Injectable, inject } from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { SPARK_BUILT_IN_ICONS } from './spark-built-in-icons';\n\n/**\n * R2-M12: SVG icon registry. The registry no longer accepts a raw string —\n * apps must pre-sanitize via `DomSanitizer.bypassSecurityTrustHtml` (or\n * `parseSvg` to validate) before calling `register`. Treating the parameter\n * as `SafeHtml` puts the explicit bypass decision in the caller's hands so a\n * developer wiring server-supplied SVG (per-tenant branding fetched from a\n * JSON endpoint) doesn't accidentally trust a `<svg><script>...</script>`\n * payload.\n *\n * The built-in icons baked into the package are still bypass-trusted internally\n * because they're known-safe at build time.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkIconRegistry {\n private sanitizer = inject(DomSanitizer);\n private icons = new Map<string, SafeHtml>();\n\n constructor() {\n for (const [name, svg] of Object.entries(SPARK_BUILT_IN_ICONS)) {\n this.icons.set(name, this.sanitizer.bypassSecurityTrustHtml(svg));\n }\n }\n\n /**\n * Registers a pre-sanitized SVG icon. Callers MUST validate or explicitly\n * trust the SVG before passing — for example:\n *\n * ```ts\n * const safe = sanitizer.bypassSecurityTrustHtml(svgFromBuildTimeConstant);\n * registry.register('app-logo', safe);\n * ```\n *\n * For server-supplied SVG, prefer a strict allow-list parser (no `<script>`,\n * no `on*` attributes, no `href=\"javascript:...\"`) before trusting.\n */\n register(name: string, svg: SafeHtml): void {\n this.icons.set(name, svg);\n }\n\n get(name: string): SafeHtml | undefined {\n return this.icons.get(name);\n }\n\n has(name: string): boolean {\n return this.icons.has(name);\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport { CustomActionDefinition, PersistentObject, SparkQuery, EntityType, filterQueryActions } from '@mintplayer/ng-spark/models';\nimport { SparkService } from './spark.service';\n\n/**\n * A query's custom actions, reachable **without the grid that usually renders them**.\n *\n * ## Why this exists\n *\n * A query's actions were only ever obtainable by rendering `<spark-query-grid>`, which loads the\n * query, resolves its entity type, fetches the actions and filters them to the ones marked for a\n * query surface — all privately. A page that wanted the same buttons somewhere else (a toolbar\n * above the card, the shell's topbar) had three bad options: duplicate the four-step resolution\n * and let it drift, reach into the grid's internals, or put the button in the wrong place.\n *\n * This exposes the resolution itself. The grid keeps rendering the default placement; a host that\n * wants a different one asks here and renders its own control.\n *\n * ## What it does not do\n *\n * It does not authorize. `/spark/actions/{type}` returns only the actions this caller may see —\n * the server filters against `security.json`, and executing re-checks — so nothing here is a gate,\n * and a host must not treat \"the list came back empty\" as anything other than a display fact.\n *\n * It also does not cache. Actions depend on the caller, and a memo keyed by type id would survive\n * a sign-out.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkQueryActionsService {\n private readonly sparkService = inject(SparkService);\n\n /**\n * The custom actions of the query named by id or alias, already filtered to those that belong on\n * a query surface.\n *\n * Returns an empty list — rather than throwing — when the query resolves to no entity type,\n * because a caller rendering a toolbar wants \"no buttons\", not a broken page.\n */\n async actionsFor(queryIdOrAlias: string): Promise<CustomActionDefinition[]> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) return [];\n\n // filterQueryActions, not a local predicate: the grid uses it too, and the `showedOn` values\n // are exactly the kind of thing a second copy gets subtly wrong — both grids once tested for\n // \"list\", a value nothing emits, and every correctly authored action rendered nowhere.\n return filterQueryActions(await this.sparkService.getCustomActions(context.entityType.id));\n }\n\n /**\n * Runs one of those actions. `selectedItemIds` are row ids, exactly as the grid posts them —\n * the server re-materializes each one through the same load path a detail page uses, so an id\n * from anywhere is treated as caller input rather than as a verified row.\n */\n async execute(\n queryIdOrAlias: string,\n actionName: string,\n options?: {\n parent?: PersistentObject;\n selectedItemIds?: string[];\n /** For a sub-query: the object whose detail page it was rendered on. See SparkService. */\n queryParent?: { id: string; type: string };\n },\n ): Promise<void> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) {\n throw new Error(\n `Cannot execute '${actionName}': query '${queryIdOrAlias}' resolves to no entity type, so there ` +\n `is nothing to execute it against.`);\n }\n\n await this.sparkService.executeCustomAction(\n context.entityType.id, actionName, options?.parent, options?.selectedItemIds, options?.queryParent,\n context.query.id);\n }\n\n /**\n * The query and the entity type its rows are mapped against — the two-step resolution both\n * methods need, kept in one place so they cannot disagree about which type an action runs on.\n */\n private async contextFor(\n queryIdOrAlias: string,\n ): Promise<{ query: SparkQuery; entityType: EntityType } | null> {\n const [query, entityTypes] = await Promise.all([\n this.sparkService.getQuery(queryIdOrAlias),\n this.sparkService.getEntityTypes(),\n ]);\n if (!query) return null;\n\n const entityType = entityTypes.find(t => t.name === query.entityType)\n ?? entityTypes.find(t => t.id === query.entityType);\n return entityType ? { query, entityType } : null;\n }\n}\n","import { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { HttpFeature, HttpFeatureKind, HttpInterceptorFn, withInterceptors } from '@angular/common/http';\nimport { PLATFORM_ID, inject } from '@angular/core';\n\n/**\n * The header carrying the viewer's IANA timezone id, e.g. `Europe/Brussels`.\n * Must stay in lockstep with `RequestTimeZoneResolver.HeaderName` on the server.\n */\nexport const SPARK_TIMEZONE_HEADER = 'X-Spark-Timezone';\n\n/**\n * The default name of the cookie carrying the same id, for requests the browser makes without any\n * script running first (a server-side render, a link opened from a mail). Must stay in lockstep\n * with `SparkTimeZoneOptions.DefaultCookieName` (`Spark:TimeZone:CookieName`) on the server.\n */\nexport const SPARK_TIMEZONE_COOKIE = 'spark-timezone';\n\n/** Options for {@link withSparkTimezone}. */\nexport interface SparkTimezoneOptions {\n /**\n * The cookie to write the viewer's zone into; `false` writes none (the header is still sent).\n * Defaults to {@link SPARK_TIMEZONE_COOKIE}. Use the same name as the server's\n * `Spark:TimeZone:CookieName`.\n */\n cookieName?: string | false;\n}\n\n/** One year: the zone only changes when the viewer travels, and each visit refreshes it. */\nconst COOKIE_MAX_AGE_SECONDS = 31_536_000;\n\n/** The server's shape check (`RequestTimeZoneResolver`): an id failing it would be ignored anyway. */\nconst ZONE_ID_SHAPE = /^[A-Za-z][A-Za-z0-9_+\\-]*(\\/[A-Za-z0-9_+\\-]+){0,2}$/;\n\n/**\n * Returns the browser's IANA zone id, or `null` when the environment cannot name one.\n *\n * An id is deliberately sent rather than a numeric offset: an offset is only valid at one instant,\n * so a server asked to render \"next Tuesday at 09:00\" from an offset captured today gets it wrong\n * across a DST boundary. A zone id carries the rules, not one sample of them.\n */\nexport function resolveBrowserTimeZone(): string | null {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || null;\n } catch {\n return null;\n }\n}\n\n/** The value of cookie `name` in `cookies` (`document.cookie`), or `null`. */\nfunction readCookie(cookies: string, name: string): string | null {\n for (const part of cookies.split(';')) {\n const eq = part.indexOf('=');\n if (eq > 0 && part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return null;\n}\n\n/**\n * Writes `zone` into cookie `name` unless it already holds it. Browser only; the server never\n * writes this cookie. `Secure` on https, `SameSite=Lax` so a link from a mail still carries it.\n */\nfunction writeTimezoneCookie(doc: Document, name: string, zone: string): void {\n if (readCookie(doc.cookie, name) === zone) return;\n const secure = doc.location?.protocol === 'https:' ? '; Secure' : '';\n doc.cookie = `${name}=${zone}; Path=/; Max-Age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax${secure}`;\n}\n\n/**\n * Builds the interceptor for {@link withSparkTimezone}. Exported for apps that compose their own\n * `withInterceptors([...])`.\n *\n * **This does not participate in reading or writing timestamps.** A `DateTimeOffset` means an\n * instant, and the browser converts in both directions on those paths. The header and cookie exist\n * for work the server does on its own — a scheduled export, a notification email, a server-side\n * render — where there is no browser in the loop to do the converting but the output still has to\n * read as local time.\n *\n * ⚠️ **Nothing happens on the server platform.** During a server-side render `Intl` names the Node\n * process's zone (UTC in a container), and a header carrying it would win over the viewer's cookie\n * on the server (spike S-TZ3, #460).\n */\nexport function createSparkTimezoneInterceptor(options: SparkTimezoneOptions = {}): HttpInterceptorFn {\n const cookieName = options.cookieName === undefined ? SPARK_TIMEZONE_COOKIE : options.cookieName;\n\n return (req, next) => {\n if (!isPlatformBrowser(inject(PLATFORM_ID))) return next(req);\n\n const zone = resolveBrowserTimeZone();\n if (!zone) return next(req);\n\n if (cookieName && ZONE_ID_SHAPE.test(zone)) {\n writeTimezoneCookie(inject(DOCUMENT), cookieName, zone);\n }\n\n // Never annotate a cross-origin call: the viewer's timezone is a (small) fingerprinting signal\n // and third-party hosts have no business receiving it.\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(req.url)) {\n try {\n if (new URL(req.url).origin !== window.location.origin) return next(req);\n } catch {\n return next(req);\n }\n }\n\n return next(req.clone({ setHeaders: { [SPARK_TIMEZONE_HEADER]: zone } }));\n };\n}\n\n/**\n * Attaches {@link SPARK_TIMEZONE_HEADER} to same-origin requests and keeps the\n * {@link SPARK_TIMEZONE_COOKIE} cookie current — with the default options. See\n * {@link createSparkTimezoneInterceptor}.\n */\nexport const sparkTimezoneInterceptor: HttpInterceptorFn = createSparkTimezoneInterceptor();\n\n/**\n * `provideHttpClient(...withSparkTimezone())` — registers the timezone interceptor.\n *\n * Spreadable alongside `withSparkAuth()`:\n * `provideHttpClient(...withSparkAuth(), ...withSparkTimezone())`.\n *\n * The cookie is written on the first request the browser app makes, so the very first server-side\n * render of a first visit has no cookie and renders in UTC.\n */\nexport function withSparkTimezone(options?: SparkTimezoneOptions): HttpFeature<HttpFeatureKind>[] {\n return [withInterceptors([options ? createSparkTimezoneInterceptor(options) : sparkTimezoneInterceptor])];\n}\n","import { inject, Injectable } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { SparkService } from './spark.service';\n\n/** The `history.state` key a list row link stores the list's own URL under. */\nexport const SPARK_RETURN_URL_STATE_KEY = 'sparkReturnUrl';\n\n/**\n * Where a detail page goes back to after its row stops existing — a Delete, or a Purge from the\n * recycle bin (#460).\n *\n * Neither \"the start page\" nor \"the previous history entry\" is the right answer: the first loses the\n * user's place, the second may be the deleted row's edit page, or another site. The right answer is\n * the list the row was opened from, so a list's row link records that list's URL in the navigation\n * state ({@link stateForRowLink}); a detail page reached otherwise (a bookmark, a reference link)\n * falls back to the row type's own list — its program-unit query, else any query over the type.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkReturnNavigationService {\n private readonly router = inject(Router);\n private readonly spark = inject(SparkService);\n\n /** Navigation state for a row link: the current URL (the list, with its `?deleted=` mode). */\n stateForRowLink(): Record<string, string> {\n return { [SPARK_RETURN_URL_STATE_KEY]: this.router.url };\n }\n\n /** The list URL the current detail page was opened from, when a row link recorded one. */\n recordedReturnUrl(): string | null {\n const state = (typeof history !== 'undefined' ? history.state : null) as Record<string, unknown> | null;\n const url = state?.[SPARK_RETURN_URL_STATE_KEY];\n // Only an in-app path: the state is the app's own, but a protocol-relative \"//host\" is not a path.\n return typeof url === 'string' && url.startsWith('/') && !url.startsWith('//') ? url : null;\n }\n\n /**\n * Navigates to the list the row came from, else to the type's list (`?deleted=only` kept when\n * `deletedView`), else to the start page. `replaceUrl`: the gone row's page is not worth a Back.\n */\n async returnToList(entityTypeName: string | null | undefined, options?: { deletedView?: boolean }): Promise<void> {\n const recorded = this.recordedReturnUrl();\n if (recorded) {\n await this.router.navigateByUrl(recorded, { replaceUrl: true });\n return;\n }\n const route = entityTypeName ? await this.listRouteFor(entityTypeName) : null;\n if (route) {\n await this.router.navigate(route, {\n replaceUrl: true,\n ...(options?.deletedView ? { queryParams: { deleted: 'only' } } : {}),\n });\n return;\n }\n await this.router.navigate(['/'], { replaceUrl: true });\n }\n\n /** `['/query', alias]` of the type's list: a menu (program-unit) query first, else any query. */\n async listRouteFor(entityTypeName: string): Promise<unknown[] | null> {\n let queries;\n try {\n queries = (await this.spark.getQueries()).filter(q => q.entityType === entityTypeName);\n } catch {\n return null;\n }\n if (!queries.length) return null;\n try {\n const units = (await this.spark.getProgramUnits()).programUnitGroups\n .flatMap(g => g.programUnits)\n .filter(u => u.type === 'query')\n .sort((a, b) => a.order - b.order);\n for (const unit of units) {\n if (queries.some(q => q.id === unit.queryId || (!!unit.alias && q.alias === unit.alias)))\n return ['/query', unit.alias || unit.queryId!];\n }\n } catch {\n // No menu: any query over the type still beats the start page.\n }\n return ['/query', queries[0].alias || queries[0].id];\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -10,14 +10,6 @@ import { SparkQueryRowAction } from "@mintplayer/ng-spark/panels";
|
|
|
10
10
|
import { ReferenceChip } from "@mintplayer/ng-spark/pipes";
|
|
11
11
|
/** Page sizes offered by every Spark grid. */
|
|
12
12
|
export declare const SPARK_GRID_PAGE_SIZES: number[];
|
|
13
|
-
/**
|
|
14
|
-
* The attributes a grid shows, in display order.
|
|
15
|
-
*
|
|
16
|
-
* Shared so the two grids cannot disagree about what "visible" means — they each had their own
|
|
17
|
-
* copy of this expression, which is the kind of thing that stays identical right up until it
|
|
18
|
-
* doesn't.
|
|
19
|
-
*/
|
|
20
|
-
export declare function visibleGridAttributes(entityType: EntityType | null): EntityAttributeDefinition[];
|
|
21
13
|
/**
|
|
22
14
|
* Initial datatable settings for a query, seeded with the query's declared sort.
|
|
23
15
|
*
|
|
@@ -547,14 +539,11 @@ export declare class SparkQueryGridComponent {
|
|
|
547
539
|
* snapshot carries them once, exactly as a paged result does.
|
|
548
540
|
*/
|
|
549
541
|
private readonly fetchedColumns;
|
|
550
|
-
/** Every column on the wire, drawn or not — what renderers and lookups resolve against. */
|
|
551
|
-
allColumns: import("@angular/core").Signal<QueryColumn[]>;
|
|
552
542
|
/**
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
* the field still draws everything.
|
|
543
|
+
* Every column on the wire — what the grid draws, and what renderers and lookups resolve against.
|
|
544
|
+
* Every column that ships is drawn (#264): there is no shipped-but-hidden column.
|
|
556
545
|
*/
|
|
557
|
-
|
|
546
|
+
allColumns: import("@angular/core").Signal<QueryColumn[]>;
|
|
558
547
|
/**
|
|
559
548
|
* True when rows do not come from this component's own fetch.
|
|
560
549
|
*
|
|
@@ -23,6 +23,11 @@ export interface ValidationErrorResponse {
|
|
|
23
23
|
* Values can be combined: ShowedOn.Query | ShowedOn.PersistentObject
|
|
24
24
|
*/
|
|
25
25
|
export declare enum ShowedOn {
|
|
26
|
+
/**
|
|
27
|
+
* Drawn on no page (#264; Vidyano's `Never`). The attribute still ships on the object, and an action can show
|
|
28
|
+
* it for one object through the runtime `showedOn` it sets on load, new or refresh.
|
|
29
|
+
*/
|
|
30
|
+
None = 0,
|
|
26
31
|
Query = 1,
|
|
27
32
|
PersistentObject = 2
|
|
28
33
|
}
|
|
@@ -86,7 +91,6 @@ export interface PersistentObjectAttribute {
|
|
|
86
91
|
dataType: string;
|
|
87
92
|
isArray?: boolean;
|
|
88
93
|
isRequired: boolean;
|
|
89
|
-
isVisible: boolean;
|
|
90
94
|
isReadOnly: boolean;
|
|
91
95
|
order: number;
|
|
92
96
|
query?: string;
|
|
@@ -249,7 +253,6 @@ export interface EntityAttributeDefinition {
|
|
|
249
253
|
description?: TranslatedString;
|
|
250
254
|
dataType: string;
|
|
251
255
|
isRequired: boolean;
|
|
252
|
-
isVisible: boolean;
|
|
253
256
|
isReadOnly: boolean;
|
|
254
257
|
order: number;
|
|
255
258
|
query?: string;
|
|
@@ -478,7 +481,7 @@ export interface QueryColumn extends SparkCellColumn {
|
|
|
478
481
|
* header sortable anyway. Neither side read it. The attribute-level `isSortable` on `EntityType`
|
|
479
482
|
* is the drag-reorder flag and stays.
|
|
480
483
|
*
|
|
481
|
-
* Absent means sortable
|
|
484
|
+
* Absent means sortable: a server predating the field still draws arrows.
|
|
482
485
|
*/
|
|
483
486
|
canSort?: boolean;
|
|
484
487
|
/** Whether the grid draws a filter cell for this column. Absent means filterable. */
|
|
@@ -489,12 +492,6 @@ export interface QueryColumn extends SparkCellColumn {
|
|
|
489
492
|
* stronger disclosure than filtering by one already known.
|
|
490
493
|
*/
|
|
491
494
|
canListDistincts?: boolean;
|
|
492
|
-
/**
|
|
493
|
-
* Whether the grid draws this column. `false` means the row carries the value but no column is
|
|
494
|
-
* rendered — for a renderer that needs a sibling value (a lock glyph beside a name) without
|
|
495
|
-
* spending a column on it. `showedOn` decides what ships; this decides what is drawn.
|
|
496
|
-
*/
|
|
497
|
-
isVisible?: boolean;
|
|
498
495
|
/** The query backing a Reference column, by name — the client's option source. */
|
|
499
496
|
query?: string;
|
|
500
497
|
}
|
|
@@ -555,8 +552,8 @@ export type SparkRow = QueryResultItem | PersistentObject | Record<string, unkno
|
|
|
555
552
|
* for its `objectId` as for its text, and a helper that discarded it would have to be replaced the
|
|
556
553
|
* first time a renderer wanted to link somewhere. `?.value` is the cost, once per call site.
|
|
557
554
|
*
|
|
558
|
-
* ⚠️ A value only reaches a grid row if its attribute is on the **query surface
|
|
559
|
-
*
|
|
555
|
+
* ⚠️ A value only reaches a grid row if its attribute is on the **query surface**, which also makes it a
|
|
556
|
+
* column: there is no shipped-but-not-drawn value (#264). An
|
|
560
557
|
* attribute marked `"showedOn": "PersistentObject"` is absent from a row by design, and this returns
|
|
561
558
|
* `undefined` for it; that is the first thing to check when a sibling read comes back empty.
|
|
562
559
|
*/
|
|
@@ -1026,7 +1023,8 @@ export interface RefreshedOption {
|
|
|
1026
1023
|
export interface AttributeOverlay {
|
|
1027
1024
|
isRequired?: boolean;
|
|
1028
1025
|
isReadOnly?: boolean;
|
|
1029
|
-
|
|
1026
|
+
/** Where the attribute is drawn for this object; an action sets it on load, new or refresh (#264). */
|
|
1027
|
+
showedOn?: ShowedOn | string;
|
|
1030
1028
|
rules?: ValidationRule[];
|
|
1031
1029
|
query?: string;
|
|
1032
1030
|
/** `undefined` means the hook did not touch the options; an empty array means there are none. */
|
|
@@ -1036,7 +1034,9 @@ export type RefreshOverlay = Record<string, AttributeOverlay>;
|
|
|
1036
1034
|
/** Applies an overlay to one attribute definition, returning a new object when anything changed. */
|
|
1037
1035
|
export declare function applyOverlay(attr: EntityAttributeDefinition, overlay: AttributeOverlay | undefined): EntityAttributeDefinition;
|
|
1038
1036
|
/**
|
|
1039
|
-
* Reads a refresh response into an overlay
|
|
1037
|
+
* Reads a refresh response into an overlay — or a loaded or new object, whose attributes carry the same
|
|
1038
|
+
* server-owned presentation: what `OnLoadAsync`/`OnNewAsync` set (`showedOn`, `isRequired`, `isReadOnly`) applies
|
|
1039
|
+
* from the first render, not only after the first refresh (#264, G5/G7).
|
|
1040
1040
|
*
|
|
1041
1041
|
* Everything here is presentation the server owns outright, so it is taken verbatim — there is no
|
|
1042
1042
|
* merging to do on this half, only on values.
|
|
@@ -141,9 +141,9 @@ export declare class SparkModerationService {
|
|
|
141
141
|
}>;
|
|
142
142
|
/**
|
|
143
143
|
* The caller's own reputation, shared by every caller in flight at once: the badge and the
|
|
144
|
-
* review-queue link both re-read on the same navigation and cost one request.
|
|
144
|
+
* review-queue link both re-read on the same navigation and cost one request. `null` signed out.
|
|
145
145
|
*/
|
|
146
|
-
ownReputation(): Promise<ModerationReputation>;
|
|
146
|
+
ownReputation(): Promise<ModerationReputation | null>;
|
|
147
147
|
/** Casts (+1 / −1) or withdraws (0) the caller's vote; resolves to the target's new state. */
|
|
148
148
|
vote(type: string, id: string, direction: -1 | 0 | 1): Promise<ModerationVoteState>;
|
|
149
149
|
/**
|
|
@@ -159,8 +159,12 @@ export declare class SparkModerationService {
|
|
|
159
159
|
unlock(type: string, id: string): Promise<void>;
|
|
160
160
|
/** What the moderator panel shows about a target, including which tools the caller holds. */
|
|
161
161
|
status(type: string, id: string): Promise<ModerationStatus>;
|
|
162
|
-
/**
|
|
163
|
-
|
|
162
|
+
/**
|
|
163
|
+
* A user's reputation; the caller's own when `userId` is omitted (with privileges and suspension).
|
|
164
|
+
* `null` for an anonymous caller: the server answers it 200 with no result rather than 401, so a
|
|
165
|
+
* badge on a page anyone may read never sends the app to the sign-in page.
|
|
166
|
+
*/
|
|
167
|
+
reputation(userId?: string): Promise<ModerationReputation | null>;
|
|
164
168
|
/** The caller's own ledger, newest first. Voters are never named; a reversal reads "Voting corrected (−N)". */
|
|
165
169
|
history(take?: number): Promise<ModerationReputationLine[]>;
|
|
166
170
|
cases(status?: string, skip?: number, take?: number): Promise<ModerationCaseSummary[]>;
|
|
@@ -117,6 +117,9 @@ export declare class AsDetailTypePipe implements PipeTransform {
|
|
|
117
117
|
/**
|
|
118
118
|
* The columns of an AsDetail table: the row type's visible attributes, in order — minus those a
|
|
119
119
|
* registered row renderer draws (`rowComponent`), when the host passes its renderer registry.
|
|
120
|
+
*
|
|
121
|
+
* An attribute the row type's model draws nowhere (`showedOn: None`, #264) is not a column. Columns are per
|
|
122
|
+
* type, so they come from the model; a runtime `showedOn` on one row cannot add or remove a column.
|
|
120
123
|
*/
|
|
121
124
|
export declare class AsDetailColumnsPipe implements PipeTransform {
|
|
122
125
|
transform(attr: EntityAttributeDefinition, asDetailTypes: Record<string, EntityType>, renderers?: readonly SparkAttributeRendererRegistration[] | null): EntityAttributeDefinition[];
|
|
@@ -16,6 +16,10 @@ export declare class SparkPoCreateComponent {
|
|
|
16
16
|
validationErrors: import("@angular/core").WritableSignal<ValidationError[]>;
|
|
17
17
|
isSaving: import("@angular/core").WritableSignal<boolean>;
|
|
18
18
|
private allEntityTypes;
|
|
19
|
+
/**
|
|
20
|
+
* The errors the form cannot show next to a field: those with no attribute, and those on an attribute the
|
|
21
|
+
* form does not draw (#264, G6). Those used to be swallowed; Vidyano promotes them to a notification too.
|
|
22
|
+
*/
|
|
19
23
|
generalErrors: import("@angular/core").Signal<ValidationError[]>;
|
|
20
24
|
/**
|
|
21
25
|
* The parent a sub-query card's New passed in the URL (`parentId`, `parentType`, `queryId`), or
|
|
@@ -108,6 +108,10 @@ export declare class SparkPoDetailComponent {
|
|
|
108
108
|
private loadItem;
|
|
109
109
|
/** (Re)loads the object, its type, rights and actions. Also what add-ons call via `context.reload()`. */
|
|
110
110
|
load(): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Drawn from the loaded object's own `showedOn`, falling back to the model's (#264, G-Q3): an action sets
|
|
113
|
+
* it per object in `OnLoadAsync` — a stolen car's police report number is shown, the model says `None`.
|
|
114
|
+
*/
|
|
111
115
|
visibleAttributes: import("@angular/core").Signal<EntityAttributeDefinition[]>;
|
|
112
116
|
private static readonly DEFAULT_TAB;
|
|
113
117
|
ungroupedAttributes: import("@angular/core").Signal<EntityAttributeDefinition[]>;
|
|
@@ -147,6 +147,11 @@ export declare class SparkPoEditComponent {
|
|
|
147
147
|
* is not the question's author edits only its translations; the server drops the rest from the
|
|
148
148
|
* save anyway). Shared by the form and {@link getEditableAttributes}, so what is drawn, what is
|
|
149
149
|
* initialised and what is saved agree. A refresh overlay can still lift it.
|
|
150
|
+
* <para>
|
|
151
|
+
* The same goes for where an attribute is drawn and whether it is required (#264, G5/G7): an action
|
|
152
|
+
* sets the runtime `showedOn`/`isRequired` in `OnLoadAsync` (a stolen car shows its police report
|
|
153
|
+
* number, required), and the form honours that from its first render rather than after a refresh.
|
|
154
|
+
* </para>
|
|
150
155
|
*/
|
|
151
156
|
formEntityType: import("@angular/core").Signal<EntityType | null>;
|
|
152
157
|
type: string;
|
|
@@ -160,6 +165,10 @@ export declare class SparkPoEditComponent {
|
|
|
160
165
|
validationErrors: import("@angular/core").WritableSignal<ValidationError[]>;
|
|
161
166
|
isSaving: import("@angular/core").WritableSignal<boolean>;
|
|
162
167
|
private allEntityTypes;
|
|
168
|
+
/**
|
|
169
|
+
* The errors the form cannot show next to a field: those with no attribute, and those on an attribute the
|
|
170
|
+
* form does not draw (#264, G6). Those used to be swallowed; Vidyano promotes them to a notification too.
|
|
171
|
+
*/
|
|
163
172
|
generalErrors: import("@angular/core").Signal<ValidationError[]>;
|
|
164
173
|
/** After a 409 was merged: what they changed, and that nothing has been saved yet. */
|
|
165
174
|
private readonly conflictNoticeText;
|
|
@@ -212,6 +221,13 @@ export declare class SparkPoEditComponent {
|
|
|
212
221
|
* then dropped on the floor and refused by the server as missing — with no way out of the form.
|
|
213
222
|
* The overlay is the form's, bound two-way, so the two halves cannot drift again.
|
|
214
223
|
*/
|
|
224
|
+
/**
|
|
225
|
+
* The attributes {@link formDataFrom} gives a slot: the editable ones, plus those the model makes editable
|
|
226
|
+
* but this object's load made read-only (#264). A refresh can lift a load-time `isReadOnly` (a stolen car's
|
|
227
|
+
* plate, once the car is back in use), and the control must then show the loaded value, not an empty
|
|
228
|
+
* required field. Only {@link getEditableAttributes} decides what is saved, so a slot alone sends nothing.
|
|
229
|
+
*/
|
|
230
|
+
private formDataAttributes;
|
|
215
231
|
getEditableAttributes(): import("@mintplayer/ng-spark/models").EntityAttributeDefinition[];
|
|
216
232
|
onSave(): Promise<void>;
|
|
217
233
|
onCancel(): void;
|
|
@@ -514,6 +514,7 @@ export declare class SparkReferencePickerComponent {
|
|
|
514
514
|
pagination: import("@angular/core").WritableSignal<PaginationResponse<QueryResultItem> | undefined>;
|
|
515
515
|
settings: import("@angular/core").WritableSignal<DatatableSettings>;
|
|
516
516
|
searchTerm: string;
|
|
517
|
+
/** The picker lists query rows, so its columns are the query's: `showedOn` includes Query (#264; not None). */
|
|
517
518
|
visibleAttributes: import("@angular/core").Signal<import("@mintplayer/ng-spark/models").EntityAttributeDefinition[]>;
|
|
518
519
|
rows: import("@angular/core").Signal<QueryResultItem[]>;
|
|
519
520
|
displayValue: import("@angular/core").Signal<string>;
|
|
@@ -103,7 +103,11 @@ export declare class SparkService {
|
|
|
103
103
|
private static readonly MAX_RETRY_DEPTH;
|
|
104
104
|
private readonly dispatcher;
|
|
105
105
|
getEntityTypes(): Promise<EntityType[]>;
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* One entity type, shaped by the caller's attribute rights. `purpose` narrows it for one form (#264, G1/G2):
|
|
108
|
+
* `'new'` leaves out what the caller may not create and does not apply edit-only denies.
|
|
109
|
+
*/
|
|
110
|
+
getEntityType(id: string, purpose?: 'new' | 'edit' | 'read'): Promise<EntityType>;
|
|
107
111
|
getEntityTypeByClrType(clrType: string): Promise<EntityType | undefined>;
|
|
108
112
|
getPermissions(entityTypeId: string): Promise<EntityPermissions>;
|
|
109
113
|
getQueries(): Promise<SparkQuery[]>;
|