@mintplayer/ng-spark 22.26.0 → 22.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/fesm2022/mintplayer-ng-spark-grid.mjs +73 -50
- 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 +19 -10
- 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 +16 -7
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +30 -14
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +45 -15
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +4 -4
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +18 -2
- 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 +12 -9
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +4 -3
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -1
- package/package.json +3 -3
- package/types/mintplayer-ng-spark-grid.d.ts +50 -46
- package/types/mintplayer-ng-spark-models.d.ts +75 -40
- 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 +12 -2
- 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-query-list.d.ts +11 -1
- package/types/mintplayer-ng-spark-services.d.ts +38 -9
- package/types/mintplayer-ng-spark-soft-delete.d.ts +1 -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 /** The query the rows were selected in, for the server's `OnDisableActionsAsync`. */\n queryId?: string;\n /** The sub-query's container, when deleting from a sub-query. */\n parentId?: string;\n parentType?: string;\n}\n\n/** Context for {@link SparkService.deleteRow}. Every field is required — see `DeleteRow.cs`. */\nexport interface DeleteRowOptions {\n asDetailAttribute: string;\n parentType: string;\n parentId: string;\n /** The row's `__sparkRowKey`, as it arrived from the server. */\n rowKey: string;\n parameters?: Record<string, string>;\n}\n\n/**\n * The union of every body the envelope helpers send.\n *\n * Named rather than inlined per helper because the New and delete-row calls carry neither a\n * `persistentObject` nor a `triggeredBy`: an inline type that did not list their fields would have\n * rejected them at the call site, and spreading them through `any` would have hidden a typo in a\n * field name the server matches by exact spelling.\n */\ntype EnvelopeRequestBody = {\n /** The entity type the request is about — the request parameter, not the submitted object's claim. */\n objectTypeId?: string;\n /** The target object's id, for update and delete. */\n id?: string;\n persistentObject?: any;\n triggeredBy?: string;\n retryResults?: RetryActionResult[];\n /** The rows of a bulk delete. */\n ids?: string[];\n} & Partial<NewObjectOptions> & Partial<DeleteManyOptions> & Partial<Omit<DeleteRowOptions, 'asDetailAttribute' | 'parentType' | 'parentId'>>;\n\n@Injectable({ providedIn: 'root' })\nexport class SparkService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly http = inject(HttpClient);\n private readonly retryActionService = inject(RetryActionService);\n\n /**\n * How many prompts one request will answer before giving up. Matches the .NET client's\n * `SparkClient.MaxRetryDepth`, and for the same reason: this is a termination bound, not a\n * capacity one. Far above any real conversation (Fleet's longest is two) and far below anything\n * that could hide a spin.\n */\n private static readonly MAX_RETRY_DEPTH = 16;\n private readonly dispatcher = inject(SparkClientOperationDispatcher);\n\n // Entity Types\n async getEntityTypes(): Promise<EntityType[]> {\n return firstValueFrom(this.http.get<EntityType[]>(`${this.baseUrl}/types`));\n }\n\n async getEntityType(id: string): Promise<EntityType> {\n return firstValueFrom(this.http.get<EntityType>(`${this.baseUrl}/types/${encodeURIComponent(id)}`));\n }\n\n async getEntityTypeByClrType(clrType: string): Promise<EntityType | undefined> {\n const types = await this.getEntityTypes();\n return types.find(t => t.clrType === clrType);\n }\n\n // Permissions\n async getPermissions(entityTypeId: string): Promise<EntityPermissions> {\n return firstValueFrom(this.http.get<EntityPermissions>(`${this.baseUrl}/permissions/${encodeURIComponent(entityTypeId)}`));\n }\n\n /** Resolved at most once; see getQueryByName. */\n #queryCatalogue?: Promise<SparkQuery[]>;\n\n // Queries\n async getQueries(): Promise<SparkQuery[]> {\n return firstValueFrom(this.http.get<SparkQuery[]>(`${this.baseUrl}/queries`));\n }\n\n async getQuery(id: string): Promise<SparkQuery> {\n return firstValueFrom(this.http.post<SparkQuery>(`${this.baseUrl}/queries/get`, { queryId: id }));\n }\n\n /**\n * Resolves a query by name, fetching the catalogue at most once per service instance.\n *\n * Without the cache this costs a full `GET /spark/queries` per call, and a form with several\n * reference attributes resolves them in parallel — so opening one page fetched the whole query\n * catalogue N times over. The catalogue is per-caller and effectively static for a page's\n * lifetime; the service is scoped to the app, so a permission change lands on the next load.\n */\n async getQueryByName(name: string): Promise<SparkQuery | undefined> {\n this.#queryCatalogue ??= this.getQueries();\n const queries = await this.#queryCatalogue;\n return queries.find(q => q.name === name);\n }\n\n async executeQuery(queryId: string, options?: ExecuteQueryOptions): Promise<QueryResult> {\n // A POST with a typed body, not a GET with a query string. `sortColumns` used to be encoded as\n // `prop:asc,other:desc` — an encoding invented because a query string has no arrays. Column\n // filtering (several columns, several selected values each) would need a second such encoding,\n // which is the reason the reads moved to POST at all.\n return this.sendRead<QueryResult>(`${this.baseUrl}/queries/execute`, {\n queryId,\n sortColumns: options?.sortColumns?.length\n ? options.sortColumns.map(c => ({ property: c.property, direction: c.direction === 'descending' ? 'desc' : 'asc' }))\n : undefined,\n parentId: options?.parentId,\n parentType: options?.parentType,\n skip: options?.skip,\n take: options?.take,\n search: options?.search,\n // Per-column value filters (#431). The shape that could not be expressed as a query string,\n // and the reason these reads are a POST at all.\n columns: options?.columns?.length ? options.columns : undefined,\n // Soft-deletion mode (#460, T2). Omitted = exclude.\n deleted: options?.deleted,\n // The parent's own mode (#460): a deleted parent opened from the recycle bin. Omitted = exclude.\n parentDeleted: options?.parentDeleted,\n });\n }\n\n /**\n * The distinct values behind one column's filter panel (#431).\n *\n * `columns` carries the OTHER columns' current filters, so the values returned are the ones still\n * reachable — offering a value that empties the grid the moment it is picked is worse than\n * omitting it.\n *\n * An empty result means either \"nothing matches\" or \"you may not enumerate this column\". The two\n * are deliberately indistinguishable, so there is nothing for a caller to branch on.\n */\n async getDistinctValues(queryId: string, column: string, options?: {\n /** Narrows the listed values themselves (the panel's own box). */\n search?: string;\n /**\n * The grid's search term, what `executeQuery` sends as `search` (#460 M15): the values come only\n * from the rows it matches, so a searched grid's panel lists what the grid shows.\n */\n querySearch?: string;\n columns?: QueryColumnFilter[];\n parentId?: string;\n parentType?: string;\n /** The same soft-deletion mode as the grid the panel belongs to (#460). */\n deleted?: SparkDeletedFilter;\n /** The same parent mode as the grid the panel belongs to (#460). */\n parentDeleted?: SparkDeletedFilter;\n }): Promise<DistinctValuesResult> {\n return this.sendRead<DistinctValuesResult>(`${this.baseUrl}/queries/distinct-values`, {\n queryId,\n column,\n search: options?.search || undefined,\n querySearch: options?.querySearch || undefined,\n columns: options?.columns?.length ? options.columns : undefined,\n deleted: options?.deleted,\n parentId: options?.parentId,\n parentType: options?.parentType,\n parentDeleted: options?.parentDeleted,\n });\n }\n\n /**\n * Runs a query identified by name.\n *\n * It used to forward only the parent, dropping skip/take/search — so every reference picker and\n * AsDetail reference column silently saw the server's default of 50 candidates, with no indication\n * that the list was cut. A picker that omits the option you are looking for, and says nothing, is\n * worse than one that fails.\n */\n async executeQueryByName(queryName: string, options?: ExecuteQueryOptions): Promise<QueryResult> {\n const query = await this.getQueryByName(queryName);\n if (!query) return { columns: [], items: [], totalItems: 0, skip: 0, take: options?.take ?? 50 };\n return this.executeQuery(query.id, options);\n }\n\n // Program Units\n async getProgramUnits(): Promise<ProgramUnitsConfiguration> {\n return firstValueFrom(this.http.get<ProgramUnitsConfiguration>(`${this.baseUrl}/program-units`));\n }\n\n // Persistent Objects\n /**\n * Loads one object. `options.deleted` (#460) lets a `ViewDeleted` holder open a soft-deleted row\n * from the recycle bin (`'include'` or `'only'`); without the right the server ignores it and a\n * deleted row stays a 404. Omitted = exclude.\n */\n async get(type: string, id: string, options?: { deleted?: SparkDeletedFilter }): Promise<PersistentObject> {\n return this.sendRead<PersistentObject>(`${this.baseUrl}/po/load`, { objectTypeId: type, id, deleted: options?.deleted });\n }\n\n /**\n * `parent` is the sub-query the New was started from — the same `parentId`/`parentType`/`queryId`\n * `newObject` was given. The server resolves and authorizes it again and hands it to the save hooks\n * as `PersistentObject.Parent`.\n */\n async create(\n type: string,\n data: Partial<PersistentObject>,\n parent?: { parentId: string; parentType: string; queryId: string },\n ): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/create`,\n { objectTypeId: type, persistentObject: data, ...(parent ?? {}) }\n );\n }\n\n async update(type: string, id: string, data: Partial<PersistentObject>): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/update`,\n { objectTypeId: type, id, persistentObject: data }\n );\n }\n\n /**\n * Asks the server to reshape an in-progress object after `triggeredBy`'s value changed.\n *\n * Writes nothing, but goes through the envelope like every other mutating call: a refresh may\n * legitimately emit notifications, and may open the retry-action prompt.\n *\n * `triggeredBy` is the attribute's name. For a trigger inside an AsDetail row it is the same\n * path form the inline validation errors use — `Jobs[2].ProfessionId`.\n */\n async refresh(type: string, data: Partial<PersistentObject>, triggeredBy: string): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/refresh`,\n { objectTypeId: type, persistentObject: data, triggeredBy }\n );\n }\n\n /**\n * Asks the server to construct a new object of `type`, so `OnNewAsync` can default it.\n *\n * Writes nothing — the object comes back unsaved, and for an AsDetail row the parent still owns\n * the save. Called by the create page for every New (#460, D19: with `parentId`/`parentType`/\n * `queryId` when started from a sub-query, whose base hook fills the parent reference), and for an\n * AsDetail row only when its type's `serverSideRowLifecycle` is on; every other row type keeps\n * building its blank row locally, which is why switching the flag off costs no request.\n *\n * ⚠️ The row comes back **keyed**: the server constructs the CLR instance, so the row-key field\n * initializer runs and `po.id` carries the key. Flattening it with `nestedPoToDict` therefore\n * populates `__sparkRowKey`, and the row is matchable on save from the moment it is added — the\n * whole reason this round-trip is worth a request.\n */\n async newObject(type: string, options?: NewObjectOptions): Promise<PersistentObject> {\n return this.postWithEnvelope<PersistentObject>(\n `${this.baseUrl}/po/new`,\n { objectTypeId: type, ...(options ?? {}) }\n );\n }\n\n /**\n * Asks whether a stored row may leave its parent's AsDetail collection.\n *\n * Deletes nothing: a resolved promise means the caller may splice the row out of the collection\n * it is editing, and the removal is persisted with the parent. A hook that refuses rejects with a\n * 400 whose `error.error.result.errors` carries the readable reason, exactly like a save.\n *\n * A POST — as everything is now. It used to be worth explaining: the framework's real delete route\n * was a catch-all that would have swallowed a sibling DELETE. The catch-all is gone with the rest\n * of the route variables.\n */\n async deleteRow(type: string, options: DeleteRowOptions): Promise<void> {\n await this.postWithEnvelope<{ removed: boolean }>(\n `${this.baseUrl}/po/delete-row`,\n { objectTypeId: type, ...options }\n );\n }\n\n async delete(type: string, id: string): Promise<void> {\n return this.postWithEnvelope<void>(\n `${this.baseUrl}/po/delete`,\n { objectTypeId: type, id }\n );\n }\n\n /**\n * Deletes several rows of one type in one request — the default Delete action on a selection\n * (`POST /spark/po/delete-many`, #460 D18). All or nothing: a 404 means some row is missing or not\n * yours to delete, a 403 that the server's `OnDisableActionsAsync` withholds Delete on the query or\n * on one of the rows, a 400 that the selection breaks the rule or the 200-row cap.\n */\n async deleteMany(type: string, ids: string[], options?: DeleteManyOptions): Promise<void> {\n return this.postWithEnvelope<void>(\n `${this.baseUrl}/po/delete-many`,\n { objectTypeId: type, ids, ...(options ?? {}) }\n );\n }\n\n // Custom Actions\n async getCustomActions(objectTypeId: string): Promise<CustomActionDefinition[]> {\n return firstValueFrom(this.http.post<CustomActionDefinition[]>(`${this.baseUrl}/actions/list`, { objectTypeId }));\n }\n\n /**\n * @param parent The object of THIS type the action is operating on — the detail-page invocation.\n * @param selectedItemIds Row ids from a query. Ids, never row objects: a row is a projection.\n * @param queryParent When invoked from a sub-query, the object whose detail page it was rendered\n * on — a DIFFERENT type (the cars listed on a company's page are Cars, the page is a Company).\n * Sent as id + type, exactly as the query endpoint names it, and resolved server-side under its\n * own type with its own Read gate.\n * @param queryId The query the selection came from, so the server can re-run it narrowed to those\n * ids and hand the action the rows the grid actually had -- index-computed columns included.\n * @returns What the action handed to `CustomActionArgs.SetResult` (#460, T5) -- the envelope's\n * `result` -- or `undefined` when it set nothing. The value is the action author's to shape: it\n * bypasses the server's redaction. A 403 means the action is disabled for this object, query or\n * selection (the server's `OnDisableActionsAsync` said so), distinct from a 404 for a row the\n * caller cannot see.\n */\n async executeCustomAction<T = unknown>(\n objectTypeId: string,\n actionName: string,\n parent?: PersistentObject,\n selectedItemIds?: string[],\n queryParent?: { id: string; type: string },\n queryId?: string,\n ): Promise<T | undefined> {\n const body: {\n objectTypeId: string; actionName: string;\n parent?: PersistentObject; selectedItemIds?: string[];\n parentId?: string; parentType?: string; queryId?: string;\n retryResults?: RetryActionResult[];\n } = { objectTypeId, actionName, parent, selectedItemIds, parentId: queryParent?.id, parentType: queryParent?.type, queryId };\n // A literal `null` result (the action set nothing) becomes undefined, so callers test one thing.\n const result = await this.postWithEnvelope<T | null>(\n `${this.baseUrl}/actions/execute`,\n body as any\n );\n return result ?? undefined;\n }\n\n // LookupReferences\n async getLookupReferences(): Promise<LookupReferenceListItem[]> {\n return firstValueFrom(this.http.get<LookupReferenceListItem[]>(`${this.baseUrl}/lookupref`));\n }\n\n async getLookupReference(name: string): Promise<LookupReference> {\n return firstValueFrom(this.http.get<LookupReference>(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`));\n }\n\n async addLookupReferenceValue(name: string, value: LookupReferenceValue): Promise<LookupReferenceValue> {\n return firstValueFrom(this.http.post<LookupReferenceValue>(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`, value));\n }\n\n async updateLookupReferenceValue(name: string, key: string, value: LookupReferenceValue): Promise<LookupReferenceValue> {\n return firstValueFrom(this.http.put<LookupReferenceValue>(\n `${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`,\n value\n ));\n }\n\n async deleteLookupReferenceValue(name: string, key: string): Promise<void> {\n return firstValueFrom(this.http.delete<void>(\n `${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`\n ));\n }\n\n /**\n * Posts to an add-on package's endpoint under the Spark base URL (`/spark` + `path`) and unwraps\n * the `ClientOperationEnvelope` exactly like the built-in mutations: operations are dispatched and\n * a 449 retry opens the retry modal. For the ng-spark add-on entry points (`soft-delete`,\n * `history`, `moderation`) whose server packages answer through core's envelope helpers\n * (`SparkAddOnEndpoints`, #460).\n */\n postEnvelope<T>(path: string, body: Record<string, unknown>): Promise<T> {\n return this.postWithEnvelope<T>(`${this.baseUrl}${path}`, body as EnvelopeRequestBody);\n }\n\n // Envelope-aware HTTP helpers.\n // All mutation endpoints (Create / Update / Delete / Execute custom action) emit the\n // ClientOperationEnvelope { result, operations } shape. These helpers unwrap the envelope,\n // dispatch any non-retry operations, and translate 449 retry-operations into the existing\n // RetryActionService modal flow.\n\n private postWithEnvelope<T>(url: string, body: EnvelopeRequestBody): Promise<T> {\n return this.sendWithEnvelope<T>(\n () => firstValueFrom(this.http.post<ClientOperationEnvelope<T>>(url, body)),\n body,\n () => this.postWithEnvelope<T>(url, body),\n );\n }\n\n /**\n * A read: posts the request, and returns the response **as-is** rather than unwrapping an\n * envelope.\n *\n * The read endpoints moved from GET to POST so their hooks could prompt, but their success\n * responses are still bare — a `PersistentObject`, a `QueryResult`. Only the 449 is enveloped,\n * because a retry has nowhere else to live. So this shares the retry loop with the mutating calls\n * and nothing else.\n */\n private async sendRead<T>(url: string, body: Record<string, unknown>): Promise<T> {\n try {\n return await firstValueFrom(this.http.post<T>(url, body));\n } catch (error) {\n return this.handleEnvelopeRetryError<T>(\n error as HttpErrorResponse,\n () => this.sendRead<T>(url, body),\n body as { retryResults?: RetryActionResult[] },\n );\n }\n }\n\n private async sendWithEnvelope<T>(\n send: () => Promise<ClientOperationEnvelope<T>>,\n body: { retryResults?: RetryActionResult[] },\n retryFn: () => Promise<T>,\n ): Promise<T> {\n try {\n const envelope = await send();\n if (envelope?.operations?.length) {\n this.dispatcher.dispatch(envelope.operations);\n }\n return envelope?.result as T;\n } catch (error) {\n return this.handleEnvelopeRetryError<T>(error as HttpErrorResponse, retryFn, body);\n }\n }\n\n private async handleEnvelopeRetryError<T>(\n error: HttpErrorResponse,\n retryFn: () => Promise<T>,\n body: { retryResults?: RetryActionResult[] }\n ): Promise<T> {\n if (error.status !== 449) throw error;\n const envelope = error.error as ClientOperationEnvelope<T> | undefined;\n if (!envelope?.operations?.length) throw error;\n\n // Dispatch any non-retry operations accumulated before the retry throw\n // so notify/refresh/etc. fire BEFORE the retry modal opens.\n const nonRetry = envelope.operations.filter(o => o.type !== 'retry');\n if (nonRetry.length) this.dispatcher.dispatch(nonRetry);\n\n const retryOp = envelope.operations.find(o => o.type === 'retry') as RetryOperation | undefined;\n if (!retryOp) throw error;\n\n // ⚠️ A hook that raises a retry WITHOUT first checking `Retry.Result` re-raises on every\n // resubmission, and this loop has no natural end: the modal reopens, the user answers, the\n // server asks again. Each round trip looks individually reasonable, so the tab spins until it is\n // closed and nothing is logged. The trap is documented in RetryFromEveryHookTests, and the .NET\n // client has had a bound since M3; this is the browser's.\n //\n // The bound is on ANSWERS ALREADY GIVEN, which is what `retryResults` is — so it counts the\n // conversation and not the component's lifetime, and two unrelated conversations do not add up.\n if ((body.retryResults?.length ?? 0) >= SparkService.MAX_RETRY_DEPTH) {\n throw new Error(\n `Gave up after answering ${SparkService.MAX_RETRY_DEPTH} retry prompts; the last was step ` +\n `${retryOp.step} (\"${retryOp.title}\"). A hook that raises a retry without checking ` +\n `Retry.Result first will do this — it re-raises on every resubmission.`);\n }\n\n const payload: RetryActionPayload = {\n type: 'retry-action',\n step: retryOp.step,\n title: retryOp.title,\n options: retryOp.options,\n defaultOption: retryOp.defaultOption ?? undefined,\n persistentObject: retryOp.persistentObject ?? undefined,\n message: retryOp.message ?? undefined,\n };\n const result = await this.retryActionService.show(payload);\n if (result.option === 'Cancel' && !payload.options.includes('Cancel')) throw error;\n\n body.retryResults = [...(body.retryResults || []), result];\n return retryFn();\n }\n}\n\n/**\n * The options every query execution takes.\n *\n * Extracted because `executeQueryByName` forwards to `executeQuery`, and while both spelled their\n * options as separate inline literals a field added to one silently failed to forward from the\n * other — which is how skip/take/search were once dropped for every reference picker.\n */\nexport interface ExecuteQueryOptions {\n sortColumns?: SortColumn[];\n parentId?: string;\n parentType?: string;\n skip?: number;\n take?: number;\n search?: string;\n /** Per-column value filters (#431). Columns AND together; values within a column OR. */\n columns?: QueryColumnFilter[];\n /**\n * Soft-deletion mode (#460): `exclude` (default), `include`, `only`. Sent only when set; the server\n * honours a widening only for callers holding `ViewDeleted` on the type.\n */\n deleted?: SparkDeletedFilter;\n /**\n * The mode the PARENT (`parentId`/`parentType`) is resolved under (#460) — `include` for a sub-query\n * on a deleted object's page, opened from the recycle bin. Independent of `deleted`, which stays the\n * rows' filter. Honoured only for callers holding `ViewDeleted` on the parent's type; anyone else\n * gets the same 404 a missing parent gives.\n */\n parentDeleted?: SparkDeletedFilter;\n}\n","/** Built-in SVG icons used by ng-spark library templates. */\nexport const SPARK_BUILT_IN_ICONS: Record<string, string> = {\n 'three-dots-vertical': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-three-dots-vertical\" viewBox=\"0 0 16 16\"><path d=\"M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0\"/></svg>',\n 'x-circle': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-circle\" viewBox=\"0 0 16 16\"><path d=\"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16\"/><path d=\"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708\"/></svg>',\n 'arrow-left': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-left\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8\"/></svg>',\n 'pencil': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-pencil\" viewBox=\"0 0 16 16\"><path d=\"M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325\"/></svg>',\n 'plus': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus\" viewBox=\"0 0 16 16\"><path d=\"M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4\"/></svg>',\n 'plus-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-plus-lg\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2\"/></svg>',\n 'search': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-search\" viewBox=\"0 0 16 16\"><path d=\"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0\"/></svg>',\n 'trash': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-trash\" viewBox=\"0 0 16 16\"><path d=\"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z\"/><path d=\"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z\"/></svg>',\n 'x-lg': '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-x-lg\" viewBox=\"0 0 16 16\"><path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z\"/></svg>',\n};\n","import { Injectable, inject } from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { SPARK_BUILT_IN_ICONS } from './spark-built-in-icons';\n\n/**\n * R2-M12: SVG icon registry. The registry no longer accepts a raw string —\n * apps must pre-sanitize via `DomSanitizer.bypassSecurityTrustHtml` (or\n * `parseSvg` to validate) before calling `register`. Treating the parameter\n * as `SafeHtml` puts the explicit bypass decision in the caller's hands so a\n * developer wiring server-supplied SVG (per-tenant branding fetched from a\n * JSON endpoint) doesn't accidentally trust a `<svg><script>...</script>`\n * payload.\n *\n * The built-in icons baked into the package are still bypass-trusted internally\n * because they're known-safe at build time.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkIconRegistry {\n private sanitizer = inject(DomSanitizer);\n private icons = new Map<string, SafeHtml>();\n\n constructor() {\n for (const [name, svg] of Object.entries(SPARK_BUILT_IN_ICONS)) {\n this.icons.set(name, this.sanitizer.bypassSecurityTrustHtml(svg));\n }\n }\n\n /**\n * Registers a pre-sanitized SVG icon. Callers MUST validate or explicitly\n * trust the SVG before passing — for example:\n *\n * ```ts\n * const safe = sanitizer.bypassSecurityTrustHtml(svgFromBuildTimeConstant);\n * registry.register('app-logo', safe);\n * ```\n *\n * For server-supplied SVG, prefer a strict allow-list parser (no `<script>`,\n * no `on*` attributes, no `href=\"javascript:...\"`) before trusting.\n */\n register(name: string, svg: SafeHtml): void {\n this.icons.set(name, svg);\n }\n\n get(name: string): SafeHtml | undefined {\n return this.icons.get(name);\n }\n\n has(name: string): boolean {\n return this.icons.has(name);\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport { CustomActionDefinition, PersistentObject, SparkQuery, EntityType, filterQueryActions } from '@mintplayer/ng-spark/models';\nimport { SparkService } from './spark.service';\n\n/**\n * A query's custom actions, reachable **without the grid that usually renders them**.\n *\n * ## Why this exists\n *\n * A query's actions were only ever obtainable by rendering `<spark-query-grid>`, which loads the\n * query, resolves its entity type, fetches the actions and filters them to the ones marked for a\n * query surface — all privately. A page that wanted the same buttons somewhere else (a toolbar\n * above the card, the shell's topbar) had three bad options: duplicate the four-step resolution\n * and let it drift, reach into the grid's internals, or put the button in the wrong place.\n *\n * This exposes the resolution itself. The grid keeps rendering the default placement; a host that\n * wants a different one asks here and renders its own control.\n *\n * ## What it does not do\n *\n * It does not authorize. `/spark/actions/{type}` returns only the actions this caller may see —\n * the server filters against `security.json`, and executing re-checks — so nothing here is a gate,\n * and a host must not treat \"the list came back empty\" as anything other than a display fact.\n *\n * It also does not cache. Actions depend on the caller, and a memo keyed by type id would survive\n * a sign-out.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkQueryActionsService {\n private readonly sparkService = inject(SparkService);\n\n /**\n * The custom actions of the query named by id or alias, already filtered to those that belong on\n * a query surface.\n *\n * Returns an empty list — rather than throwing — when the query resolves to no entity type,\n * because a caller rendering a toolbar wants \"no buttons\", not a broken page.\n */\n async actionsFor(queryIdOrAlias: string): Promise<CustomActionDefinition[]> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) return [];\n\n // filterQueryActions, not a local predicate: the grid uses it too, and the `showedOn` values\n // are exactly the kind of thing a second copy gets subtly wrong — both grids once tested for\n // \"list\", a value nothing emits, and every correctly authored action rendered nowhere.\n return filterQueryActions(await this.sparkService.getCustomActions(context.entityType.id));\n }\n\n /**\n * Runs one of those actions. `selectedItemIds` are row ids, exactly as the grid posts them —\n * the server re-materializes each one through the same load path a detail page uses, so an id\n * from anywhere is treated as caller input rather than as a verified row.\n */\n async execute(\n queryIdOrAlias: string,\n actionName: string,\n options?: {\n parent?: PersistentObject;\n selectedItemIds?: string[];\n /** For a sub-query: the object whose detail page it was rendered on. See SparkService. */\n queryParent?: { id: string; type: string };\n },\n ): Promise<void> {\n const context = await this.contextFor(queryIdOrAlias);\n if (!context) {\n throw new Error(\n `Cannot execute '${actionName}': query '${queryIdOrAlias}' resolves to no entity type, so there ` +\n `is nothing to execute it against.`);\n }\n\n await this.sparkService.executeCustomAction(\n context.entityType.id, actionName, options?.parent, options?.selectedItemIds, options?.queryParent,\n context.query.id);\n }\n\n /**\n * The query and the entity type its rows are mapped against — the two-step resolution both\n * methods need, kept in one place so they cannot disagree about which type an action runs on.\n */\n private async contextFor(\n queryIdOrAlias: string,\n ): Promise<{ query: SparkQuery; entityType: EntityType } | null> {\n const [query, entityTypes] = await Promise.all([\n this.sparkService.getQuery(queryIdOrAlias),\n this.sparkService.getEntityTypes(),\n ]);\n if (!query) return null;\n\n const entityType = entityTypes.find(t => t.name === query.entityType)\n ?? entityTypes.find(t => t.id === query.entityType);\n return entityType ? { query, entityType } : null;\n }\n}\n","import { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { HttpFeature, HttpFeatureKind, HttpInterceptorFn, withInterceptors } from '@angular/common/http';\nimport { PLATFORM_ID, inject } from '@angular/core';\n\n/**\n * The header carrying the viewer's IANA timezone id, e.g. `Europe/Brussels`.\n * Must stay in lockstep with `RequestTimeZoneResolver.HeaderName` on the server.\n */\nexport const SPARK_TIMEZONE_HEADER = 'X-Spark-Timezone';\n\n/**\n * The default name of the cookie carrying the same id, for requests the browser makes without any\n * script running first (a server-side render, a link opened from a mail). Must stay in lockstep\n * with `SparkTimeZoneOptions.DefaultCookieName` (`Spark:TimeZone:CookieName`) on the server.\n */\nexport const SPARK_TIMEZONE_COOKIE = 'spark-timezone';\n\n/** Options for {@link withSparkTimezone}. */\nexport interface SparkTimezoneOptions {\n /**\n * The cookie to write the viewer's zone into; `false` writes none (the header is still sent).\n * Defaults to {@link SPARK_TIMEZONE_COOKIE}. Use the same name as the server's\n * `Spark:TimeZone:CookieName`.\n */\n cookieName?: string | false;\n}\n\n/** One year: the zone only changes when the viewer travels, and each visit refreshes it. */\nconst COOKIE_MAX_AGE_SECONDS = 31_536_000;\n\n/** The server's shape check (`RequestTimeZoneResolver`): an id failing it would be ignored anyway. */\nconst ZONE_ID_SHAPE = /^[A-Za-z][A-Za-z0-9_+\\-]*(\\/[A-Za-z0-9_+\\-]+){0,2}$/;\n\n/**\n * Returns the browser's IANA zone id, or `null` when the environment cannot name one.\n *\n * An id is deliberately sent rather than a numeric offset: an offset is only valid at one instant,\n * so a server asked to render \"next Tuesday at 09:00\" from an offset captured today gets it wrong\n * across a DST boundary. A zone id carries the rules, not one sample of them.\n */\nexport function resolveBrowserTimeZone(): string | null {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || null;\n } catch {\n return null;\n }\n}\n\n/** The value of cookie `name` in `cookies` (`document.cookie`), or `null`. */\nfunction readCookie(cookies: string, name: string): string | null {\n for (const part of cookies.split(';')) {\n const eq = part.indexOf('=');\n if (eq > 0 && part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();\n }\n return null;\n}\n\n/**\n * Writes `zone` into cookie `name` unless it already holds it. Browser only; the server never\n * writes this cookie. `Secure` on https, `SameSite=Lax` so a link from a mail still carries it.\n */\nfunction writeTimezoneCookie(doc: Document, name: string, zone: string): void {\n if (readCookie(doc.cookie, name) === zone) return;\n const secure = doc.location?.protocol === 'https:' ? '; Secure' : '';\n doc.cookie = `${name}=${zone}; Path=/; Max-Age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax${secure}`;\n}\n\n/**\n * Builds the interceptor for {@link withSparkTimezone}. Exported for apps that compose their own\n * `withInterceptors([...])`.\n *\n * **This does not participate in reading or writing timestamps.** A `DateTimeOffset` means an\n * instant, and the browser converts in both directions on those paths. The header and cookie exist\n * for work the server does on its own — a scheduled export, a notification email, a server-side\n * render — where there is no browser in the loop to do the converting but the output still has to\n * read as local time.\n *\n * ⚠️ **Nothing happens on the server platform.** During a server-side render `Intl` names the Node\n * process's zone (UTC in a container), and a header carrying it would win over the viewer's cookie\n * on the server (spike S-TZ3, #460).\n */\nexport function createSparkTimezoneInterceptor(options: SparkTimezoneOptions = {}): HttpInterceptorFn {\n const cookieName = options.cookieName === undefined ? SPARK_TIMEZONE_COOKIE : options.cookieName;\n\n return (req, next) => {\n if (!isPlatformBrowser(inject(PLATFORM_ID))) return next(req);\n\n const zone = resolveBrowserTimeZone();\n if (!zone) return next(req);\n\n if (cookieName && ZONE_ID_SHAPE.test(zone)) {\n writeTimezoneCookie(inject(DOCUMENT), cookieName, zone);\n }\n\n // Never annotate a cross-origin call: the viewer's timezone is a (small) fingerprinting signal\n // and third-party hosts have no business receiving it.\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(req.url)) {\n try {\n if (new URL(req.url).origin !== window.location.origin) return next(req);\n } catch {\n return next(req);\n }\n }\n\n return next(req.clone({ setHeaders: { [SPARK_TIMEZONE_HEADER]: zone } }));\n };\n}\n\n/**\n * Attaches {@link SPARK_TIMEZONE_HEADER} to same-origin requests and keeps the\n * {@link SPARK_TIMEZONE_COOKIE} cookie current — with the default options. See\n * {@link createSparkTimezoneInterceptor}.\n */\nexport const sparkTimezoneInterceptor: HttpInterceptorFn = createSparkTimezoneInterceptor();\n\n/**\n * `provideHttpClient(...withSparkTimezone())` — registers the timezone interceptor.\n *\n * Spreadable alongside `withSparkAuth()`:\n * `provideHttpClient(...withSparkAuth(), ...withSparkTimezone())`.\n *\n * The cookie is written on the first request the browser app makes, so the very first server-side\n * render of a first visit has no cookie and renders in UTC.\n */\nexport function withSparkTimezone(options?: SparkTimezoneOptions): HttpFeature<HttpFeatureKind>[] {\n return [withInterceptors([options ? createSparkTimezoneInterceptor(options) : sparkTimezoneInterceptor])];\n}\n","import { inject, Injectable } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { SparkService } from './spark.service';\n\n/** The `history.state` key a list row link stores the list's own URL under. */\nexport const SPARK_RETURN_URL_STATE_KEY = 'sparkReturnUrl';\n\n/**\n * Where a detail page goes back to after its row stops existing — a Delete, or a Purge from the\n * recycle bin (#460).\n *\n * Neither \"the start page\" nor \"the previous history entry\" is the right answer: the first loses the\n * user's place, the second may be the deleted row's edit page, or another site. The right answer is\n * the list the row was opened from, so a list's row link records that list's URL in the navigation\n * state ({@link stateForRowLink}); a detail page reached otherwise (a bookmark, a reference link)\n * falls back to the row type's own list — its program-unit query, else any query over the type.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkReturnNavigationService {\n private readonly router = inject(Router);\n private readonly spark = inject(SparkService);\n\n /** Navigation state for a row link: the current URL (the list, with its `?deleted=` mode). */\n stateForRowLink(): Record<string, string> {\n return { [SPARK_RETURN_URL_STATE_KEY]: this.router.url };\n }\n\n /** The list URL the current detail page was opened from, when a row link recorded one. */\n recordedReturnUrl(): string | null {\n const state = (typeof history !== 'undefined' ? history.state : null) as Record<string, unknown> | null;\n const url = state?.[SPARK_RETURN_URL_STATE_KEY];\n // Only an in-app path: the state is the app's own, but a protocol-relative \"//host\" is not a path.\n return typeof url === 'string' && url.startsWith('/') && !url.startsWith('//') ? url : null;\n }\n\n /**\n * Navigates to the list the row came from, else to the type's list (`?deleted=only` kept when\n * `deletedView`), else to the start page. `replaceUrl`: the gone row's page is not worth a Back.\n */\n async returnToList(entityTypeName: string | null | undefined, options?: { deletedView?: boolean }): Promise<void> {\n const recorded = this.recordedReturnUrl();\n if (recorded) {\n await this.router.navigateByUrl(recorded, { replaceUrl: true });\n return;\n }\n const route = entityTypeName ? await this.listRouteFor(entityTypeName) : null;\n if (route) {\n await this.router.navigate(route, {\n replaceUrl: true,\n ...(options?.deletedView ? { queryParams: { deleted: 'only' } } : {}),\n });\n return;\n }\n await this.router.navigate(['/'], { replaceUrl: true });\n }\n\n /** `['/query', alias]` of the type's list: a menu (program-unit) query first, else any query. */\n async listRouteFor(entityTypeName: string): Promise<unknown[] | null> {\n let queries;\n try {\n queries = (await this.spark.getQueries()).filter(q => q.entityType === entityTypeName);\n } catch {\n return null;\n }\n if (!queries.length) return null;\n try {\n const units = (await this.spark.getProgramUnits()).programUnitGroups\n .flatMap(g => g.programUnits)\n .filter(u => u.type === 'query')\n .sort((a, b) => a.order - b.order);\n for (const unit of units) {\n if (queries.some(q => q.id === unit.queryId || (!!unit.alias && q.alias === unit.alias)))\n return ['/query', unit.alias || unit.queryId!];\n }\n } catch {\n // No menu: any query over the type still beats the start page.\n }\n return ['/query', queries[0].alias || queries[0].id];\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-services.mjs","names":[],"sources":["../../services/src/retry-action.service.ts","../../services/src/spark-language.service.ts","../../services/src/spark-streaming.service.ts","../../services/src/spark.service.ts","../../services/src/spark-built-in-icons.ts","../../services/src/spark-icon-registry.ts","../../services/src/spark-query-actions.service.ts","../../services/src/spark-timezone.interceptor.ts","../../services/src/spark-return-navigation.service.ts"],"sourcesContent":["import { Injectable, signal } from '@angular/core';\nimport { RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\n\n@Injectable({ providedIn: 'root' })\nexport class RetryActionService {\n private resolveRetry: ((result: RetryActionResult) => void) | null = null;\n\n payload = signal<RetryActionPayload | null>(null);\n\n show(payload: RetryActionPayload): Promise<RetryActionResult> {\n this.payload.set(payload);\n return new Promise(resolve => { this.resolveRetry = resolve; });\n }\n\n respond(result: RetryActionResult): void {\n this.payload.set(null);\n this.resolveRetry?.(result);\n this.resolveRetry = null;\n }\n}\n","import { Injectable, inject, signal } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { TranslatedString, currentLanguage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\ninterface CultureConfiguration {\n languages: Record<string, TranslatedString>;\n defaultLanguage: string;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class SparkLanguageService {\n private readonly http = inject(HttpClient);\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly currentLang = signal('en');\n private readonly translationsMap = signal<Record<string, TranslatedString>>({});\n\n readonly language = this.currentLang.asReadonly();\n readonly languages = signal<Record<string, TranslatedString>>({});\n\n constructor() {\n this.loadCulture();\n this.loadTranslations();\n }\n\n private async loadCulture(): Promise<void> {\n const config = await firstValueFrom(this.http.get<CultureConfiguration>(`${this.baseUrl}/culture`));\n this.languages.set(config.languages);\n const saved = localStorage.getItem('spark-lang');\n const lang = saved ?? config.defaultLanguage;\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n }\n\n private async loadTranslations(): Promise<void> {\n const t = await firstValueFrom(this.http.get<Record<string, TranslatedString>>(`${this.baseUrl}/translations`));\n this.translationsMap.set(t);\n }\n\n setLanguage(lang: string) {\n this.currentLang.set(lang);\n currentLanguage.set(lang);\n localStorage.setItem('spark-lang', lang);\n }\n\n resolve(ts: TranslatedString | undefined): string {\n if (!ts) return '';\n const lang = this.currentLang();\n return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? '';\n }\n\n t(key: string): string {\n const ts = this.translationsMap()[key];\n return this.resolve(ts) || key;\n }\n}\n","import { inject, Injectable, NgZone } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { StreamingMessage } from '@mintplayer/ng-spark/models';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n@Injectable({ providedIn: 'root' })\nexport class SparkStreamingService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly ngZone = inject(NgZone);\n\n connectToStreamingQuery(queryId: string): Observable<StreamingMessage> {\n return new Observable<StreamingMessage>(subscriber => {\n let ws: WebSocket | null = null;\n let retryCount = 0;\n let retryTimeout: ReturnType<typeof setTimeout> | null = null;\n let closed = false;\n const maxRetries = 10;\n const maxDelay = 30000;\n\n const connect = () => {\n if (closed) return;\n\n const url = this.buildWebSocketUrl(queryId);\n ws = new WebSocket(url);\n\n ws.onopen = () => {\n retryCount = 0; // Reset on successful connect\n };\n\n ws.onmessage = (event) => {\n try {\n const message: StreamingMessage = JSON.parse(event.data);\n this.ngZone.run(() => subscriber.next(message));\n } catch {\n // Ignore malformed messages\n }\n };\n\n ws.onerror = () => {\n // Error will trigger onclose\n };\n\n ws.onclose = (event) => {\n if (closed) return;\n\n // Don't reconnect on normal closure\n if (event.code === 1000) {\n this.ngZone.run(() => subscriber.complete());\n return;\n }\n\n // Reconnect with exponential backoff\n if (retryCount < maxRetries) {\n const delay = Math.min(1000 * Math.pow(2, retryCount), maxDelay);\n retryCount++;\n retryTimeout = setTimeout(() => connect(), delay);\n } else {\n this.ngZone.run(() => subscriber.error(new Error('WebSocket connection failed after maximum retries')));\n }\n };\n };\n\n connect();\n\n // Teardown: close WebSocket on unsubscribe\n return () => {\n closed = true;\n if (retryTimeout) {\n clearTimeout(retryTimeout);\n }\n if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {\n ws.close(1000, 'Client unsubscribed');\n }\n };\n });\n }\n\n private buildWebSocketUrl(queryId: string): string {\n const encodedId = encodeURIComponent(queryId);\n const path = `${this.baseUrl}/queries/${encodedId}/stream`;\n\n // If baseUrl is absolute (starts with http/https), replace protocol\n if (this.baseUrl.startsWith('http://')) {\n return path.replace(/^http:\\/\\//, 'ws://');\n }\n if (this.baseUrl.startsWith('https://')) {\n return path.replace(/^https:\\/\\//, 'wss://');\n }\n\n // Relative URL — construct from window.location\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n return `${protocol}//${window.location.host}${path}`;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { HttpClient, HttpErrorResponse } from '@angular/common/http';\nimport { firstValueFrom } from 'rxjs';\nimport { CustomActionDefinition, DistinctValuesResult, EntityPermissions, EntityType, LookupReference, LookupReferenceListItem, LookupReferenceValue, PersistentObject, ProgramUnitsConfiguration, QueryColumnFilter, QueryResult, SparkDeletedFilter, SparkQuery, RetryActionPayload, RetryActionResult } from '@mintplayer/ng-spark/models';\nimport { ClientOperationEnvelope, RetryOperation, SparkClientOperationDispatcher } from '@mintplayer/ng-spark/client-operations';\nimport { SortColumn } from '@mintplayer/pagination';\nimport { RetryActionService } from './retry-action.service';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\n\n/**\n * Context for {@link SparkService.newObject}. Everything is optional: a standalone New sends an\n * empty body, and an AsDetail row on a parent that has never been saved sends no `parentId` —\n * which is the honest answer, not a gap. The server hands the hook a null parent rather than\n * trusting the client's copy of one.\n */\nexport interface NewObjectOptions {\n /** Name of the parent's AsDetail attribute the row is for; absent for a standalone New. */\n asDetailAttribute?: string;\n /** The parent's entity type — required whenever `asDetailAttribute` is set. */\n parentType?: string;\n /** The parent's id, absent while the parent is itself unsaved. */\n parentId?: string;\n /** Free-form arguments, e.g. which variant a New menu chose. */\n parameters?: Record<string, string>;\n /**\n * The sub-query New was started from (#460, D19), with `parentType` and `parentId` and no\n * `asDetailAttribute`. The server loads the parent, checks the parent's type declares this\n * sub-query, and runs `OnNewAsync` with it — whose base fills the reference to the parent.\n */\n queryId?: string;\n}\n\n/** Where a bulk delete was started from; see {@link SparkService.deleteMany}. */\nexport interface DeleteManyOptions {\n /**\n * The query the rows were selected in. Required (#467, D12): the server fetches the rows through it\n * and applies its `OnDisableActionsAsync` decision; without it the call is a 400.\n */\n queryId: string;\n /** The sub-query's container, when deleting from a sub-query. */\n parentId?: string;\n parentType?: string;\n /** One reason for the whole batch, recorded on every soft-deleted row (#467, D20). */\n reason?: string;\n}\n\n/** A row and the version of it the caller saw (#467, D14): what a bulk delete names. */\nexport interface SparkRowVersion {\n id: string;\n /** The row's `etag` from the query result. */\n etag: string;\n}\n\n/** Context for {@link SparkService.deleteRow}. Every field is required — see `DeleteRow.cs`. */\nexport interface DeleteRowOptions {\n asDetailAttribute: string;\n parentType: string;\n parentId: string;\n /** The row's `__sparkRowKey`, as it arrived from the server. */\n rowKey: string;\n parameters?: Record<string, string>;\n}\n\n/**\n * The union of every body the envelope helpers send.\n *\n * Named rather than inlined per helper because the New and delete-row calls carry neither a\n * `persistentObject` nor a `triggeredBy`: an inline type that did not list their fields would have\n * rejected them at the call site, and spreading them through `any` would have hidden a typo in a\n * field name the server matches by exact spelling.\n */\ntype EnvelopeRequestBody = {\n /** The entity type the request is about — the request parameter, not the submitted object's claim. */\n objectTypeId?: string;\n /** The target object's id, for update and delete. */\n id?: string;\n persistentObject?: any;\n triggeredBy?: string;\n retryResults?: RetryActionResult[];\n /** The version being deleted, for a single delete. */\n etag?: string;\n /** The rows of a bulk delete, each with its version. */\n items?: SparkRowVersion[];\n} & Partial<NewObjectOptions> & Partial<DeleteManyOptions> & Partial<Omit<DeleteRowOptions, 'asDetailAttribute' | 'parentType' | 'parentId'>>;\n\n@Injectable({ providedIn: 'root' })\nexport class SparkService {\n private readonly config = inject(SPARK_CONFIG, { optional: true });\n private readonly baseUrl = this.config?.baseUrl ?? '/spark';\n private readonly http = inject(HttpClient);\n private readonly retryActionService = inject(RetryActionService);\n\n /**\n * How many prompts one request will answer before giving up. Matches the .NET client's\n * `SparkClient.MaxRetryDepth`, and for the same reason: this is a termination bound, not a\n * capacity one. Far above any real conversation (Fleet's longest is two) and far below anything\n * that could hide a spin.\n */\n private static readonly MAX_RETRY_DEPTH = 16;\n private readonly dispatcher = inject(SparkClientOperationDispatcher);\n\n // Entity Types\n async getEntityTypes(): Promise<EntityType[]> {\n return firstValueFrom(this.http.get<EntityType[]>(`${this.baseUrl}/types`));\n }\n\n /**\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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"}
|
|
@@ -13,10 +13,11 @@ var SparkSoftDeleteService = class SparkSoftDeleteService {
|
|
|
13
13
|
id
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
-
async purge(type, id) {
|
|
16
|
+
async purge(type, id, etag) {
|
|
17
17
|
await this.spark.postEnvelope("/po/purge", {
|
|
18
18
|
objectTypeId: type,
|
|
19
|
-
id
|
|
19
|
+
id,
|
|
20
|
+
etag
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
@@ -166,7 +167,7 @@ var SparkSoftDeleteActionsComponent = class SparkSoftDeleteActionsComponent {
|
|
|
166
167
|
if (!confirm(this.lang.t("softDelete.confirmPurge"))) return;
|
|
167
168
|
const ctx = this.context();
|
|
168
169
|
await this.run(async () => {
|
|
169
|
-
await this.softDelete.purge(ctx.type, ctx.id);
|
|
170
|
+
await this.softDelete.purge(ctx.type, ctx.id, ctx.item.etag ?? "");
|
|
170
171
|
await this.returnNavigation.returnToList(ctx.entityType?.name, { deletedView: true });
|
|
171
172
|
});
|
|
172
173
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-soft-delete.mjs","names":[],"sources":["../../soft-delete/src/spark-soft-delete.service.ts","../../soft-delete/src/spark-deleted-toggle.component.ts","../../soft-delete/src/spark-soft-delete-actions.component.ts","../../soft-delete/src/provide-spark-soft-delete.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport { PersistentObject } from '@mintplayer/ng-spark/models';\nimport { SparkService } from '@mintplayer/ng-spark/services';\n\n/**\n * Client for the SoftDelete package's endpoints (`MintPlayer.Spark.SoftDelete`, #460 T1). Ids travel\n * in the body — the route table is literal and Raven ids contain slashes.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkSoftDeleteService {\n private readonly spark = inject(SparkService);\n\n /**\n * `POST /spark/po/restore` → the restored (live) object. Requires `Restore/T` and a deleted row;\n * anything else is 404 (or 401 anonymous), a restore withheld by `OnDisableActionsAsync` is 403.\n */\n restore(type: string, id: string): Promise<PersistentObject> {\n return this.spark.postEnvelope<PersistentObject>('/po/restore', { objectTypeId: type, id });\n }\n\n /**\n * `POST /spark/po/purge` → 204. Requires `Purge/T` and a deleted row. Removes the document and\n * every revision of it; it cannot be undone.\n */\n async purge(type: string, id: string): Promise<void> {\n await this.spark.postEnvelope<void>('/po/purge', { objectTypeId: type, id });\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkQueryListContext } from '@mintplayer/ng-spark/panels';\n\n/**\n * The query page's \"Deleted\" toggle (#460): switches the list into the recycle bin\n * (`deleted: only`) and back. Rendered only for holders of `ViewDeleted/T` — for anyone else the\n * server would ignore the widening anyway, so offering it would show the same rows under a\n * misleading label.\n *\n * Registered by {@link provideSparkSoftDelete} as a `SPARK_QUERY_LIST_ACTIONS` entry; usable on its\n * own in a custom page with any {@link SparkQueryListContext}.\n */\n@Component({\n selector: 'spark-deleted-toggle',\n imports: [TranslateKeyPipe, SparkIconComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (visible()) {\n <button type=\"button\"\n class=\"btn spark-deleted-toggle\"\n [class.btn-secondary]=\"active()\"\n [class.btn-outline-secondary]=\"!active()\"\n [attr.aria-pressed]=\"active()\"\n (click)=\"toggle()\">\n <spark-icon name=\"trash\" /> {{ 'softDelete.showDeleted' | t }}\n </button>\n }\n `,\n})\nexport class SparkDeletedToggleComponent {\n context = input.required<SparkQueryListContext>();\n\n protected readonly visible = computed(() =>\n !!this.context().entityType && this.context().permissions?.canViewDeleted === true);\n\n protected readonly active = computed(() => this.context().deleted === 'only');\n\n toggle(): void {\n this.context().setDeleted(this.active() ? 'exclude' : 'only');\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkReturnNavigationService } from '@mintplayer/ng-spark/services';\nimport { SparkDetailContext } from '@mintplayer/ng-spark/panels';\nimport { SparkSoftDeleteService } from './spark-soft-delete.service';\n\n/**\n * Restore and Purge on the detail page of a row opened from the recycle bin (#460).\n *\n * Shown only when the page loaded the row with `?deleted=only` (every row in the recycle bin is\n * deleted) and the caller holds `Restore/T` / `Purge/T`. The server re-checks both — the row must be\n * deleted and the row gate must pass — so a refusal here is the 404/403 it answers, surfaced inline.\n *\n * Registered by {@link provideSparkSoftDelete} as a `SPARK_DETAIL_ACTIONS` entry.\n */\n@Component({\n selector: 'spark-soft-delete-actions',\n imports: [TranslateKeyPipe, SparkIconComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (deletedView()) {\n <span class=\"d-inline-flex gap-2 align-items-center\">\n @if (canRestore()) {\n <button type=\"button\" class=\"btn btn-success spark-restore\" [disabled]=\"busy()\" (click)=\"restore()\">\n {{ 'softDelete.restore' | t }}\n </button>\n }\n @if (canPurge()) {\n <button type=\"button\" class=\"btn btn-danger spark-purge\" [disabled]=\"busy()\" (click)=\"purge()\">\n <spark-icon name=\"trash\" /> {{ 'softDelete.purge' | t }}\n </button>\n }\n @if (error(); as message) {\n <span class=\"text-danger small spark-soft-delete-error\" role=\"alert\">{{ message }}</span>\n }\n </span>\n }\n `,\n})\nexport class SparkSoftDeleteActionsComponent {\n private readonly softDelete = inject(SparkSoftDeleteService);\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute);\n private readonly returnNavigation = inject(SparkReturnNavigationService);\n private readonly lang = inject(SparkLanguageService);\n\n context = input.required<SparkDetailContext>();\n\n protected readonly deletedView = computed(() => this.context().deleted === 'only');\n protected readonly canRestore = computed(() => this.context().permissions?.canRestore === true);\n protected readonly canPurge = computed(() => this.context().permissions?.canPurge === true);\n\n protected readonly busy = signal(false);\n protected readonly error = signal<string | null>(null);\n\n async restore(): Promise<void> {\n if (this.busy()) return;\n const ctx = this.context();\n await this.run(async () => {\n await this.softDelete.restore(ctx.type, ctx.id);\n // Same page, live mode: dropping `?deleted` makes the detail page re-read the row normally.\n // The list this row was opened from travels on: a later Delete still returns there.\n const returnUrl = this.returnNavigation.recordedReturnUrl();\n await this.router.navigate([], {\n relativeTo: this.route,\n queryParams: { deleted: null },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n ...(returnUrl ? { state: { [SPARK_RETURN_URL_STATE_KEY]: returnUrl } } : {}),\n });\n });\n }\n\n async purge(): Promise<void> {\n if (this.busy()) return;\n if (!confirm(this.lang.t('softDelete.confirmPurge'))) return;\n const ctx = this.context();\n await this.run(async () => {\n await this.softDelete.purge(ctx.type, ctx.id);\n // The row no longer exists: back to the recycle bin it was opened from — the recorded list, else\n // the type's list in `?deleted=only`. Never \"the previous page\", which may be anything.\n await this.returnNavigation.returnToList(ctx.entityType?.name, { deletedView: true });\n });\n }\n\n private async run(work: () => Promise<void>): Promise<void> {\n this.busy.set(true);\n this.error.set(null);\n try {\n await work();\n } catch (e) {\n const err = e as HttpErrorResponse;\n this.error.set(err?.status === 404\n ? this.lang.t('softDelete.unavailable')\n : (err?.error?.error || err?.message || this.lang.t('common.actionFailed')));\n } finally {\n this.busy.set(false);\n }\n }\n}\n","import { Provider } from '@angular/core';\nimport { provideSparkDetailActions, provideSparkQueryListActions } from '@mintplayer/ng-spark/panels';\nimport { SparkDeletedToggleComponent } from './spark-deleted-toggle.component';\nimport { SparkSoftDeleteActionsComponent } from './spark-soft-delete-actions.component';\n\n/**\n * Adds the SoftDelete UI (#460) to the pages `sparkRoutes()` routes to:\n *\n * - the query page gets a **Deleted** toggle (holders of `ViewDeleted/T`) that lists the recycle bin\n * (`?deleted=only`); its rows open with the same mode;\n * - the detail page of such a row gets **Restore** (`Restore/T`) and **Purge** (`Purge/T`).\n *\n * Needs the server package `MintPlayer.Spark.SoftDelete`. Add to the app's providers:\n * `providers: [provideSpark(...), provideSparkSoftDelete()]`.\n */\nexport function provideSparkSoftDelete(): Provider[] {\n return [\n ...provideSparkQueryListActions({ id: 'spark-soft-delete-toggle', component: SparkDeletedToggleComponent, priority: 55 }),\n ...provideSparkDetailActions({ id: 'spark-soft-delete-actions', component: SparkSoftDeleteActionsComponent, priority: 4 }),\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-soft-delete.mjs","names":[],"sources":["../../soft-delete/src/spark-soft-delete.service.ts","../../soft-delete/src/spark-deleted-toggle.component.ts","../../soft-delete/src/spark-soft-delete-actions.component.ts","../../soft-delete/src/provide-spark-soft-delete.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport { PersistentObject } from '@mintplayer/ng-spark/models';\nimport { SparkService } from '@mintplayer/ng-spark/services';\n\n/**\n * Client for the SoftDelete package's endpoints (`MintPlayer.Spark.SoftDelete`, #460 T1). Ids travel\n * in the body — the route table is literal and Raven ids contain slashes.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkSoftDeleteService {\n private readonly spark = inject(SparkService);\n\n /**\n * `POST /spark/po/restore` → the restored (live) object. Requires `Restore/T` and a deleted row;\n * anything else is 404 (or 401 anonymous), a restore withheld by `OnDisableActionsAsync` is 403.\n */\n restore(type: string, id: string): Promise<PersistentObject> {\n return this.spark.postEnvelope<PersistentObject>('/po/restore', { objectTypeId: type, id });\n }\n\n /**\n * `POST /spark/po/purge` → 204. Requires `Purge/T` and a deleted row. Removes the document and\n * every revision of it; it cannot be undone.\n */\n async purge(type: string, id: string, etag: string): Promise<void> {\n await this.spark.postEnvelope<void>('/po/purge', { objectTypeId: type, id, etag });\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkQueryListContext } from '@mintplayer/ng-spark/panels';\n\n/**\n * The query page's \"Deleted\" toggle (#460): switches the list into the recycle bin\n * (`deleted: only`) and back. Rendered only for holders of `ViewDeleted/T` — for anyone else the\n * server would ignore the widening anyway, so offering it would show the same rows under a\n * misleading label.\n *\n * Registered by {@link provideSparkSoftDelete} as a `SPARK_QUERY_LIST_ACTIONS` entry; usable on its\n * own in a custom page with any {@link SparkQueryListContext}.\n */\n@Component({\n selector: 'spark-deleted-toggle',\n imports: [TranslateKeyPipe, SparkIconComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (visible()) {\n <button type=\"button\"\n class=\"btn spark-deleted-toggle\"\n [class.btn-secondary]=\"active()\"\n [class.btn-outline-secondary]=\"!active()\"\n [attr.aria-pressed]=\"active()\"\n (click)=\"toggle()\">\n <spark-icon name=\"trash\" /> {{ 'softDelete.showDeleted' | t }}\n </button>\n }\n `,\n})\nexport class SparkDeletedToggleComponent {\n context = input.required<SparkQueryListContext>();\n\n protected readonly visible = computed(() =>\n !!this.context().entityType && this.context().permissions?.canViewDeleted === true);\n\n protected readonly active = computed(() => this.context().deleted === 'only');\n\n toggle(): void {\n this.context().setDeleted(this.active() ? 'exclude' : 'only');\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkReturnNavigationService } from '@mintplayer/ng-spark/services';\nimport { SparkDetailContext } from '@mintplayer/ng-spark/panels';\nimport { SparkSoftDeleteService } from './spark-soft-delete.service';\n\n/**\n * Restore and Purge on the detail page of a row opened from the recycle bin (#460).\n *\n * Shown only when the page loaded the row with `?deleted=only` (every row in the recycle bin is\n * deleted) and the caller holds `Restore/T` / `Purge/T`. The server re-checks both — the row must be\n * deleted and the row gate must pass — so a refusal here is the 404/403 it answers, surfaced inline.\n *\n * Registered by {@link provideSparkSoftDelete} as a `SPARK_DETAIL_ACTIONS` entry.\n */\n@Component({\n selector: 'spark-soft-delete-actions',\n imports: [TranslateKeyPipe, SparkIconComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (deletedView()) {\n <span class=\"d-inline-flex gap-2 align-items-center\">\n @if (canRestore()) {\n <button type=\"button\" class=\"btn btn-success spark-restore\" [disabled]=\"busy()\" (click)=\"restore()\">\n {{ 'softDelete.restore' | t }}\n </button>\n }\n @if (canPurge()) {\n <button type=\"button\" class=\"btn btn-danger spark-purge\" [disabled]=\"busy()\" (click)=\"purge()\">\n <spark-icon name=\"trash\" /> {{ 'softDelete.purge' | t }}\n </button>\n }\n @if (error(); as message) {\n <span class=\"text-danger small spark-soft-delete-error\" role=\"alert\">{{ message }}</span>\n }\n </span>\n }\n `,\n})\nexport class SparkSoftDeleteActionsComponent {\n private readonly softDelete = inject(SparkSoftDeleteService);\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute);\n private readonly returnNavigation = inject(SparkReturnNavigationService);\n private readonly lang = inject(SparkLanguageService);\n\n context = input.required<SparkDetailContext>();\n\n protected readonly deletedView = computed(() => this.context().deleted === 'only');\n protected readonly canRestore = computed(() => this.context().permissions?.canRestore === true);\n protected readonly canPurge = computed(() => this.context().permissions?.canPurge === true);\n\n protected readonly busy = signal(false);\n protected readonly error = signal<string | null>(null);\n\n async restore(): Promise<void> {\n if (this.busy()) return;\n const ctx = this.context();\n await this.run(async () => {\n await this.softDelete.restore(ctx.type, ctx.id);\n // Same page, live mode: dropping `?deleted` makes the detail page re-read the row normally.\n // The list this row was opened from travels on: a later Delete still returns there.\n const returnUrl = this.returnNavigation.recordedReturnUrl();\n await this.router.navigate([], {\n relativeTo: this.route,\n queryParams: { deleted: null },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n ...(returnUrl ? { state: { [SPARK_RETURN_URL_STATE_KEY]: returnUrl } } : {}),\n });\n });\n }\n\n async purge(): Promise<void> {\n if (this.busy()) return;\n if (!confirm(this.lang.t('softDelete.confirmPurge'))) return;\n const ctx = this.context();\n await this.run(async () => {\n await this.softDelete.purge(ctx.type, ctx.id, ctx.item.etag ?? '');\n // The row no longer exists: back to the recycle bin it was opened from — the recorded list, else\n // the type's list in `?deleted=only`. Never \"the previous page\", which may be anything.\n await this.returnNavigation.returnToList(ctx.entityType?.name, { deletedView: true });\n });\n }\n\n private async run(work: () => Promise<void>): Promise<void> {\n this.busy.set(true);\n this.error.set(null);\n try {\n await work();\n } catch (e) {\n const err = e as HttpErrorResponse;\n this.error.set(err?.status === 404\n ? this.lang.t('softDelete.unavailable')\n : (err?.error?.error || err?.message || this.lang.t('common.actionFailed')));\n } finally {\n this.busy.set(false);\n }\n }\n}\n","import { Provider } from '@angular/core';\nimport { provideSparkDetailActions, provideSparkQueryListActions } from '@mintplayer/ng-spark/panels';\nimport { SparkDeletedToggleComponent } from './spark-deleted-toggle.component';\nimport { SparkSoftDeleteActionsComponent } from './spark-soft-delete-actions.component';\n\n/**\n * Adds the SoftDelete UI (#460) to the pages `sparkRoutes()` routes to:\n *\n * - the query page gets a **Deleted** toggle (holders of `ViewDeleted/T`) that lists the recycle bin\n * (`?deleted=only`); its rows open with the same mode;\n * - the detail page of such a row gets **Restore** (`Restore/T`) and **Purge** (`Purge/T`).\n *\n * Needs the server package `MintPlayer.Spark.SoftDelete`. Add to the app's providers:\n * `providers: [provideSpark(...), provideSparkSoftDelete()]`.\n */\nexport function provideSparkSoftDelete(): Provider[] {\n return [\n ...provideSparkQueryListActions({ id: 'spark-soft-delete-toggle', component: SparkDeletedToggleComponent, priority: 55 }),\n ...provideSparkDetailActions({ id: 'spark-soft-delete-actions', component: SparkSoftDeleteActionsComponent, priority: 4 }),\n ];\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mintplayer/ng-spark",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "22.
|
|
4
|
+
"version": "22.28.0",
|
|
5
5
|
"description": "Angular component library for MintPlayer.Spark CRUD applications",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"@angular/common": "^22.0.0",
|
|
15
15
|
"@angular/router": "^22.0.0",
|
|
16
16
|
"@angular/forms": "^22.0.0",
|
|
17
|
-
"@mintplayer/ng-bootstrap": "^22.
|
|
18
|
-
"@mintplayer/web-components": "^2.
|
|
17
|
+
"@mintplayer/ng-bootstrap": "^22.21.0",
|
|
18
|
+
"@mintplayer/web-components": "^2.18.0",
|
|
19
19
|
"@mintplayer/pagination": "*",
|
|
20
20
|
"rxjs": "~7.8.0"
|
|
21
21
|
},
|