@mintplayer/ng-spark 22.22.0 → 22.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +181 -0
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +100 -47
  3. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +308 -329
  5. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +175 -135
  7. package/fesm2022/mintplayer-ng-spark-column-filter.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-grid.mjs +2259 -1117
  9. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  10. package/fesm2022/mintplayer-ng-spark-history.mjs +611 -0
  11. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -0
  12. package/fesm2022/mintplayer-ng-spark-icon.mjs +64 -27
  13. package/fesm2022/mintplayer-ng-spark-icon.mjs.map +1 -1
  14. package/fesm2022/mintplayer-ng-spark-models.mjs +365 -753
  15. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-moderation.mjs +1212 -0
  17. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -0
  18. package/fesm2022/mintplayer-ng-spark-panels.mjs +52 -0
  19. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -0
  20. package/fesm2022/mintplayer-ng-spark-pipes.mjs +1122 -765
  21. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  22. package/fesm2022/mintplayer-ng-spark-po-create.mjs +273 -169
  23. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +786 -410
  25. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +292 -284
  27. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-po-form.mjs +2187 -1225
  29. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-query-list.mjs +516 -303
  31. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-renderers.mjs +17 -55
  33. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +216 -177
  35. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark-routes.mjs +24 -28
  37. package/fesm2022/mintplayer-ng-spark-routes.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-services.mjs +625 -640
  39. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-shell.mjs +1013 -397
  41. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +294 -0
  43. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -0
  44. package/fesm2022/mintplayer-ng-spark.mjs +13 -50
  45. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  46. package/package.json +17 -1
  47. package/types/mintplayer-ng-spark-attribute-description.d.ts +25 -18
  48. package/types/mintplayer-ng-spark-client-operations.d.ts +133 -160
  49. package/types/mintplayer-ng-spark-column-filter.d.ts +73 -55
  50. package/types/mintplayer-ng-spark-grid.d.ts +885 -448
  51. package/types/mintplayer-ng-spark-history.d.ts +223 -0
  52. package/types/mintplayer-ng-spark-icon.d.ts +15 -13
  53. package/types/mintplayer-ng-spark-models.d.ts +624 -573
  54. package/types/mintplayer-ng-spark-moderation.d.ts +402 -0
  55. package/types/mintplayer-ng-spark-panels.d.ts +108 -0
  56. package/types/mintplayer-ng-spark-pipes.d.ts +120 -149
  57. package/types/mintplayer-ng-spark-po-create.d.ts +46 -37
  58. package/types/mintplayer-ng-spark-po-detail.d.ts +177 -117
  59. package/types/mintplayer-ng-spark-po-edit.d.ts +64 -65
  60. package/types/mintplayer-ng-spark-po-form.d.ts +507 -371
  61. package/types/mintplayer-ng-spark-query-list.d.ts +118 -82
  62. package/types/mintplayer-ng-spark-renderers.d.ts +79 -85
  63. package/types/mintplayer-ng-spark-retry-action-modal.d.ts +31 -34
  64. package/types/mintplayer-ng-spark-routes.d.ts +7 -11
  65. package/types/mintplayer-ng-spark-services.d.ts +377 -261
  66. package/types/mintplayer-ng-spark-shell.d.ts +209 -128
  67. package/types/mintplayer-ng-spark-soft-delete.d.ts +90 -0
  68. package/types/mintplayer-ng-spark.d.ts +7 -12
@@ -1,647 +1,632 @@
1
- import * as i0 from '@angular/core';
2
- import { signal, Injectable, inject, NgZone } from '@angular/core';
3
- import { HttpClient, withInterceptors } from '@angular/common/http';
4
- import { firstValueFrom, Observable } from 'rxjs';
5
- import { currentLanguage, filterQueryActions } from '@mintplayer/ng-spark/models';
6
- import { SPARK_CONFIG } from '@mintplayer/ng-spark';
7
- import { SparkClientOperationDispatcher } from '@mintplayer/ng-spark/client-operations';
8
- import { DomSanitizer } from '@angular/platform-browser';
9
-
10
- class RetryActionService {
11
- resolveRetry = null;
12
- payload = signal(null, /* @ts-ignore */
13
- ...(ngDevMode ? [{ debugName: "payload" }] : /* istanbul ignore next */ []));
14
- show(payload) {
15
- this.payload.set(payload);
16
- return new Promise(resolve => { this.resolveRetry = resolve; });
17
- }
18
- respond(result) {
19
- this.payload.set(null);
20
- this.resolveRetry?.(result);
21
- this.resolveRetry = null;
22
- }
23
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: RetryActionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
24
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: RetryActionService, providedIn: 'root' });
25
- }
26
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: RetryActionService, decorators: [{
27
- type: Injectable,
28
- args: [{ providedIn: 'root' }]
29
- }] });
30
-
31
- class SparkLanguageService {
32
- http = inject(HttpClient);
33
- config = inject(SPARK_CONFIG, { optional: true });
34
- baseUrl = this.config?.baseUrl ?? '/spark';
35
- currentLang = signal('en', /* @ts-ignore */
36
- ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
37
- translationsMap = signal({}, /* @ts-ignore */
38
- ...(ngDevMode ? [{ debugName: "translationsMap" }] : /* istanbul ignore next */ []));
39
- language = this.currentLang.asReadonly();
40
- languages = signal({}, /* @ts-ignore */
41
- ...(ngDevMode ? [{ debugName: "languages" }] : /* istanbul ignore next */ []));
42
- constructor() {
43
- this.loadCulture();
44
- this.loadTranslations();
45
- }
46
- async loadCulture() {
47
- const config = await firstValueFrom(this.http.get(`${this.baseUrl}/culture`));
48
- this.languages.set(config.languages);
49
- const saved = localStorage.getItem('spark-lang');
50
- const lang = saved ?? config.defaultLanguage;
51
- this.currentLang.set(lang);
52
- currentLanguage.set(lang);
53
- }
54
- async loadTranslations() {
55
- const t = await firstValueFrom(this.http.get(`${this.baseUrl}/translations`));
56
- this.translationsMap.set(t);
57
- }
58
- setLanguage(lang) {
59
- this.currentLang.set(lang);
60
- currentLanguage.set(lang);
61
- localStorage.setItem('spark-lang', lang);
62
- }
63
- resolve(ts) {
64
- if (!ts)
65
- return '';
66
- const lang = this.currentLang();
67
- return ts[lang] ?? ts['en'] ?? Object.values(ts)[0] ?? '';
68
- }
69
- t(key) {
70
- const ts = this.translationsMap()[key];
71
- return this.resolve(ts) || key;
72
- }
73
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkLanguageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
74
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkLanguageService, providedIn: 'root' });
75
- }
76
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkLanguageService, decorators: [{
77
- type: Injectable,
78
- args: [{ providedIn: 'root' }]
79
- }], ctorParameters: () => [] });
80
-
81
- class SparkStreamingService {
82
- config = inject(SPARK_CONFIG, { optional: true });
83
- baseUrl = this.config?.baseUrl ?? '/spark';
84
- ngZone = inject(NgZone);
85
- connectToStreamingQuery(queryId) {
86
- return new Observable(subscriber => {
87
- let ws = null;
88
- let retryCount = 0;
89
- let retryTimeout = null;
90
- let closed = false;
91
- const maxRetries = 10;
92
- const maxDelay = 30000;
93
- const connect = () => {
94
- if (closed)
95
- return;
96
- const url = this.buildWebSocketUrl(queryId);
97
- ws = new WebSocket(url);
98
- ws.onopen = () => {
99
- retryCount = 0; // Reset on successful connect
100
- };
101
- ws.onmessage = (event) => {
102
- try {
103
- const message = JSON.parse(event.data);
104
- this.ngZone.run(() => subscriber.next(message));
105
- }
106
- catch {
107
- // Ignore malformed messages
108
- }
109
- };
110
- ws.onerror = () => {
111
- // Error will trigger onclose
112
- };
113
- ws.onclose = (event) => {
114
- if (closed)
115
- return;
116
- // Don't reconnect on normal closure
117
- if (event.code === 1000) {
118
- this.ngZone.run(() => subscriber.complete());
119
- return;
120
- }
121
- // Reconnect with exponential backoff
122
- if (retryCount < maxRetries) {
123
- const delay = Math.min(1000 * Math.pow(2, retryCount), maxDelay);
124
- retryCount++;
125
- retryTimeout = setTimeout(() => connect(), delay);
126
- }
127
- else {
128
- this.ngZone.run(() => subscriber.error(new Error('WebSocket connection failed after maximum retries')));
129
- }
130
- };
131
- };
132
- connect();
133
- // Teardown: close WebSocket on unsubscribe
134
- return () => {
135
- closed = true;
136
- if (retryTimeout) {
137
- clearTimeout(retryTimeout);
138
- }
139
- if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
140
- ws.close(1000, 'Client unsubscribed');
141
- }
142
- };
143
- });
144
- }
145
- buildWebSocketUrl(queryId) {
146
- const encodedId = encodeURIComponent(queryId);
147
- const path = `${this.baseUrl}/queries/${encodedId}/stream`;
148
- // If baseUrl is absolute (starts with http/https), replace protocol
149
- if (this.baseUrl.startsWith('http://')) {
150
- return path.replace(/^http:\/\//, 'ws://');
151
- }
152
- if (this.baseUrl.startsWith('https://')) {
153
- return path.replace(/^https:\/\//, 'wss://');
154
- }
155
- // Relative URL — construct from window.location
156
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
157
- return `${protocol}//${window.location.host}${path}`;
158
- }
159
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkStreamingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
160
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkStreamingService, providedIn: 'root' });
161
- }
162
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkStreamingService, decorators: [{
163
- type: Injectable,
164
- args: [{ providedIn: 'root' }]
165
- }] });
166
-
167
- class SparkService {
168
- config = inject(SPARK_CONFIG, { optional: true });
169
- baseUrl = this.config?.baseUrl ?? '/spark';
170
- http = inject(HttpClient);
171
- retryActionService = inject(RetryActionService);
172
- /**
173
- * How many prompts one request will answer before giving up. Matches the .NET client's
174
- * `SparkClient.MaxRetryDepth`, and for the same reason: this is a termination bound, not a
175
- * capacity one. Far above any real conversation (Fleet's longest is two) and far below anything
176
- * that could hide a spin.
177
- */
178
- static MAX_RETRY_DEPTH = 16;
179
- dispatcher = inject(SparkClientOperationDispatcher);
180
- // Entity Types
181
- async getEntityTypes() {
182
- return firstValueFrom(this.http.get(`${this.baseUrl}/types`));
183
- }
184
- async getEntityType(id) {
185
- return firstValueFrom(this.http.get(`${this.baseUrl}/types/${encodeURIComponent(id)}`));
186
- }
187
- async getEntityTypeByClrType(clrType) {
188
- const types = await this.getEntityTypes();
189
- return types.find(t => t.clrType === clrType);
190
- }
191
- // Permissions
192
- async getPermissions(entityTypeId) {
193
- return firstValueFrom(this.http.get(`${this.baseUrl}/permissions/${encodeURIComponent(entityTypeId)}`));
194
- }
195
- /** Resolved at most once; see getQueryByName. */
196
- #queryCatalogue;
197
- // Queries
198
- async getQueries() {
199
- return firstValueFrom(this.http.get(`${this.baseUrl}/queries`));
200
- }
201
- async getQuery(id) {
202
- return firstValueFrom(this.http.post(`${this.baseUrl}/queries/get`, { queryId: id }));
203
- }
204
- /**
205
- * Resolves a query by name, fetching the catalogue at most once per service instance.
206
- *
207
- * Without the cache this costs a full `GET /spark/queries` per call, and a form with several
208
- * reference attributes resolves them in parallel — so opening one page fetched the whole query
209
- * catalogue N times over. The catalogue is per-caller and effectively static for a page's
210
- * lifetime; the service is scoped to the app, so a permission change lands on the next load.
211
- */
212
- async getQueryByName(name) {
213
- this.#queryCatalogue ??= this.getQueries();
214
- const queries = await this.#queryCatalogue;
215
- return queries.find(q => q.name === name);
216
- }
217
- async executeQuery(queryId, options) {
218
- // A POST with a typed body, not a GET with a query string. `sortColumns` used to be encoded as
219
- // `prop:asc,other:desc` — an encoding invented because a query string has no arrays. Column
220
- // filtering (several columns, several selected values each) would need a second such encoding,
221
- // which is the reason the reads moved to POST at all.
222
- return this.sendRead(`${this.baseUrl}/queries/execute`, {
223
- queryId,
224
- sortColumns: options?.sortColumns?.length
225
- ? options.sortColumns.map(c => ({ property: c.property, direction: c.direction === 'descending' ? 'desc' : 'asc' }))
226
- : undefined,
227
- parentId: options?.parentId,
228
- parentType: options?.parentType,
229
- skip: options?.skip,
230
- take: options?.take,
231
- search: options?.search,
232
- // Per-column value filters (#431). The shape that could not be expressed as a query string,
233
- // and the reason these reads are a POST at all.
234
- columns: options?.columns?.length ? options.columns : undefined,
235
- });
236
- }
237
- /**
238
- * The distinct values behind one column's filter panel (#431).
239
- *
240
- * `columns` carries the OTHER columns' current filters, so the values returned are the ones still
241
- * reachable — offering a value that empties the grid the moment it is picked is worse than
242
- * omitting it.
243
- *
244
- * An empty result means either "nothing matches" or "you may not enumerate this column". The two
245
- * are deliberately indistinguishable, so there is nothing for a caller to branch on.
246
- */
247
- async getDistinctValues(queryId, column, options) {
248
- return this.sendRead(`${this.baseUrl}/queries/distinct-values`, {
249
- queryId,
250
- column,
251
- search: options?.search || undefined,
252
- columns: options?.columns?.length ? options.columns : undefined,
253
- parentId: options?.parentId,
254
- parentType: options?.parentType,
255
- });
256
- }
257
- /**
258
- * Runs a query identified by name.
259
- *
260
- * It used to forward only the parent, dropping skip/take/search — so every reference picker and
261
- * AsDetail reference column silently saw the server's default of 50 candidates, with no indication
262
- * that the list was cut. A picker that omits the option you are looking for, and says nothing, is
263
- * worse than one that fails.
264
- */
265
- async executeQueryByName(queryName, options) {
266
- const query = await this.getQueryByName(queryName);
267
- if (!query)
268
- return { columns: [], items: [], totalItems: 0, skip: 0, take: options?.take ?? 50 };
269
- return this.executeQuery(query.id, options);
270
- }
271
- // Program Units
272
- async getProgramUnits() {
273
- return firstValueFrom(this.http.get(`${this.baseUrl}/program-units`));
274
- }
275
- // Persistent Objects
276
- async get(type, id) {
277
- return this.sendRead(`${this.baseUrl}/po/load`, { objectTypeId: type, id });
278
- }
279
- async create(type, data) {
280
- return this.postWithEnvelope(`${this.baseUrl}/po/create`, { objectTypeId: type, persistentObject: data });
281
- }
282
- async update(type, id, data) {
283
- return this.postWithEnvelope(`${this.baseUrl}/po/update`, { objectTypeId: type, id, persistentObject: data });
284
- }
285
- /**
286
- * Asks the server to reshape an in-progress object after `triggeredBy`'s value changed.
287
- *
288
- * Writes nothing, but goes through the envelope like every other mutating call: a refresh may
289
- * legitimately emit notifications, and may open the retry-action prompt.
290
- *
291
- * `triggeredBy` is the attribute's name. For a trigger inside an AsDetail row it is the same
292
- * path form the inline validation errors use — `Jobs[2].ProfessionId`.
293
- */
294
- async refresh(type, data, triggeredBy) {
295
- return this.postWithEnvelope(`${this.baseUrl}/po/refresh`, { objectTypeId: type, persistentObject: data, triggeredBy });
296
- }
297
- /**
298
- * Asks the server to construct a new object of `type`, so `OnNewAsync` can default it.
299
- *
300
- * Writes nothing — the object comes back unsaved, and for an AsDetail row the parent still owns
301
- * the save. Only called for a row type whose `serverSideRowLifecycle` is on; every other type
302
- * keeps building its blank row locally, which is why switching the flag off costs no request.
303
- *
304
- * ⚠️ The row comes back **keyed**: the server constructs the CLR instance, so the row-key field
305
- * initializer runs and `po.id` carries the key. Flattening it with `nestedPoToDict` therefore
306
- * populates `__sparkRowKey`, and the row is matchable on save from the moment it is added — the
307
- * whole reason this round-trip is worth a request.
308
- */
309
- async newObject(type, options) {
310
- return this.postWithEnvelope(`${this.baseUrl}/po/new`, { objectTypeId: type, ...(options ?? {}) });
311
- }
312
- /**
313
- * Asks whether a stored row may leave its parent's AsDetail collection.
314
- *
315
- * Deletes nothing: a resolved promise means the caller may splice the row out of the collection
316
- * it is editing, and the removal is persisted with the parent. A hook that refuses rejects with a
317
- * 400 whose `error.error.result.errors` carries the readable reason, exactly like a save.
318
- *
319
- * A POST — as everything is now. It used to be worth explaining: the framework's real delete route
320
- * was a catch-all that would have swallowed a sibling DELETE. The catch-all is gone with the rest
321
- * of the route variables.
322
- */
323
- async deleteRow(type, options) {
324
- await this.postWithEnvelope(`${this.baseUrl}/po/delete-row`, { objectTypeId: type, ...options });
325
- }
326
- async delete(type, id) {
327
- return this.postWithEnvelope(`${this.baseUrl}/po/delete`, { objectTypeId: type, id });
328
- }
329
- // Custom Actions
330
- async getCustomActions(objectTypeId) {
331
- return firstValueFrom(this.http.post(`${this.baseUrl}/actions/list`, { objectTypeId }));
332
- }
333
- /**
334
- * @param parent The object of THIS type the action is operating on — the detail-page invocation.
335
- * @param selectedItemIds Row ids from a query. Ids, never row objects: a row is a projection.
336
- * @param queryParent When invoked from a sub-query, the object whose detail page it was rendered
337
- * on — a DIFFERENT type (the cars listed on a company's page are Cars, the page is a Company).
338
- * Sent as id + type, exactly as the query endpoint names it, and resolved server-side under its
339
- * own type with its own Read gate.
340
- * @param queryId The query the selection came from, so the server can re-run it narrowed to those
341
- * ids and hand the action the rows the grid actually had -- index-computed columns included.
342
- */
343
- async executeCustomAction(objectTypeId, actionName, parent, selectedItemIds, queryParent, queryId) {
344
- const body = { objectTypeId, actionName, parent, selectedItemIds, parentId: queryParent?.id, parentType: queryParent?.type, queryId };
345
- return this.postWithEnvelope(`${this.baseUrl}/actions/execute`, body);
346
- }
347
- // LookupReferences
348
- async getLookupReferences() {
349
- return firstValueFrom(this.http.get(`${this.baseUrl}/lookupref`));
350
- }
351
- async getLookupReference(name) {
352
- return firstValueFrom(this.http.get(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`));
353
- }
354
- async addLookupReferenceValue(name, value) {
355
- return firstValueFrom(this.http.post(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`, value));
356
- }
357
- async updateLookupReferenceValue(name, key, value) {
358
- return firstValueFrom(this.http.put(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`, value));
359
- }
360
- async deleteLookupReferenceValue(name, key) {
361
- return firstValueFrom(this.http.delete(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`));
362
- }
363
- // Envelope-aware HTTP helpers.
364
- // All mutation endpoints (Create / Update / Delete / Execute custom action) emit the
365
- // ClientOperationEnvelope { result, operations } shape. These helpers unwrap the envelope,
366
- // dispatch any non-retry operations, and translate 449 retry-operations into the existing
367
- // RetryActionService modal flow.
368
- postWithEnvelope(url, body) {
369
- return this.sendWithEnvelope(() => firstValueFrom(this.http.post(url, body)), body, () => this.postWithEnvelope(url, body));
370
- }
371
- /**
372
- * A read: posts the request, and returns the response **as-is** rather than unwrapping an
373
- * envelope.
374
- *
375
- * The read endpoints moved from GET to POST so their hooks could prompt, but their success
376
- * responses are still bare — a `PersistentObject`, a `QueryResult`. Only the 449 is enveloped,
377
- * because a retry has nowhere else to live. So this shares the retry loop with the mutating calls
378
- * and nothing else.
379
- */
380
- async sendRead(url, body) {
381
- try {
382
- return await firstValueFrom(this.http.post(url, body));
383
- }
384
- catch (error) {
385
- return this.handleEnvelopeRetryError(error, () => this.sendRead(url, body), body);
386
- }
387
- }
388
- async sendWithEnvelope(send, body, retryFn) {
389
- try {
390
- const envelope = await send();
391
- if (envelope?.operations?.length) {
392
- this.dispatcher.dispatch(envelope.operations);
393
- }
394
- return envelope?.result;
395
- }
396
- catch (error) {
397
- return this.handleEnvelopeRetryError(error, retryFn, body);
398
- }
399
- }
400
- async handleEnvelopeRetryError(error, retryFn, body) {
401
- if (error.status !== 449)
402
- throw error;
403
- const envelope = error.error;
404
- if (!envelope?.operations?.length)
405
- throw error;
406
- // Dispatch any non-retry operations accumulated before the retry throw
407
- // so notify/refresh/etc. fire BEFORE the retry modal opens.
408
- const nonRetry = envelope.operations.filter(o => o.type !== 'retry');
409
- if (nonRetry.length)
410
- this.dispatcher.dispatch(nonRetry);
411
- const retryOp = envelope.operations.find(o => o.type === 'retry');
412
- if (!retryOp)
413
- throw error;
414
- // ⚠️ A hook that raises a retry WITHOUT first checking `Retry.Result` re-raises on every
415
- // resubmission, and this loop has no natural end: the modal reopens, the user answers, the
416
- // server asks again. Each round trip looks individually reasonable, so the tab spins until it is
417
- // closed and nothing is logged. The trap is documented in RetryFromEveryHookTests, and the .NET
418
- // client has had a bound since M3; this is the browser's.
419
- //
420
- // The bound is on ANSWERS ALREADY GIVEN, which is what `retryResults` is — so it counts the
421
- // conversation and not the component's lifetime, and two unrelated conversations do not add up.
422
- if ((body.retryResults?.length ?? 0) >= SparkService.MAX_RETRY_DEPTH) {
423
- throw new Error(`Gave up after answering ${SparkService.MAX_RETRY_DEPTH} retry prompts; the last was step ` +
424
- `${retryOp.step} ("${retryOp.title}"). A hook that raises a retry without checking ` +
425
- `Retry.Result first will do this — it re-raises on every resubmission.`);
426
- }
427
- const payload = {
428
- type: 'retry-action',
429
- step: retryOp.step,
430
- title: retryOp.title,
431
- options: retryOp.options,
432
- defaultOption: retryOp.defaultOption ?? undefined,
433
- persistentObject: retryOp.persistentObject ?? undefined,
434
- message: retryOp.message ?? undefined,
435
- };
436
- const result = await this.retryActionService.show(payload);
437
- if (result.option === 'Cancel' && !payload.options.includes('Cancel'))
438
- throw error;
439
- body.retryResults = [...(body.retryResults || []), result];
440
- return retryFn();
441
- }
442
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
443
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkService, providedIn: 'root' });
444
- }
445
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkService, decorators: [{
446
- type: Injectable,
447
- args: [{ providedIn: 'root' }]
448
- }] });
449
-
450
- /** Built-in SVG icons used by ng-spark library templates. */
1
+ import * as i0 from "@angular/core";
2
+ import { Injectable, NgZone, PLATFORM_ID, inject, signal } from "@angular/core";
3
+ import { HttpClient, withInterceptors } from "@angular/common/http";
4
+ import { Observable, firstValueFrom } from "rxjs";
5
+ import { currentLanguage, filterQueryActions } from "@mintplayer/ng-spark/models";
6
+ import { SPARK_CONFIG } from "@mintplayer/ng-spark";
7
+ import { SparkClientOperationDispatcher } from "@mintplayer/ng-spark/client-operations";
8
+ import { DomSanitizer } from "@angular/platform-browser";
9
+ import { DOCUMENT, isPlatformBrowser } from "@angular/common";
10
+ import { Router } from "@angular/router";
11
+ var RetryActionService = class RetryActionService {
12
+ resolveRetry = null;
13
+ payload = signal(null, ...ngDevMode ? [{ debugName: "payload" }] : /* istanbul ignore next */ []);
14
+ show(payload) {
15
+ this.payload.set(payload);
16
+ return new Promise((resolve) => {
17
+ this.resolveRetry = resolve;
18
+ });
19
+ }
20
+ respond(result) {
21
+ this.payload.set(null);
22
+ this.resolveRetry?.(result);
23
+ this.resolveRetry = null;
24
+ }
25
+ static ɵfac = i0.ɵɵngDeclareFactory({
26
+ minVersion: "12.0.0",
27
+ version: "22.2.0",
28
+ ngImport: i0,
29
+ type: RetryActionService,
30
+ deps: [],
31
+ target: i0.ɵɵFactoryTarget.Injectable
32
+ });
33
+ static ɵprov = i0.ɵɵngDeclareInjectable({
34
+ minVersion: "12.0.0",
35
+ version: "22.2.0",
36
+ ngImport: i0,
37
+ type: RetryActionService,
38
+ providedIn: "root"
39
+ });
40
+ };
41
+ i0.ɵɵngDeclareClassMetadata({
42
+ minVersion: "12.0.0",
43
+ version: "22.2.0",
44
+ ngImport: i0,
45
+ type: RetryActionService,
46
+ decorators: [{
47
+ type: Injectable,
48
+ args: [{ providedIn: "root" }]
49
+ }]
50
+ });
51
+ var SparkLanguageService = class SparkLanguageService {
52
+ http = inject(HttpClient);
53
+ config = inject(SPARK_CONFIG, { optional: true });
54
+ baseUrl = this.config?.baseUrl ?? "/spark";
55
+ currentLang = signal("en", ...ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []);
56
+ translationsMap = signal({}, ...ngDevMode ? [{ debugName: "translationsMap" }] : /* istanbul ignore next */ []);
57
+ language = this.currentLang.asReadonly();
58
+ languages = signal({}, ...ngDevMode ? [{ debugName: "languages" }] : /* istanbul ignore next */ []);
59
+ constructor() {
60
+ this.loadCulture();
61
+ this.loadTranslations();
62
+ }
63
+ async loadCulture() {
64
+ const config = await firstValueFrom(this.http.get(`${this.baseUrl}/culture`));
65
+ this.languages.set(config.languages);
66
+ const lang = localStorage.getItem("spark-lang") ?? config.defaultLanguage;
67
+ this.currentLang.set(lang);
68
+ currentLanguage.set(lang);
69
+ }
70
+ async loadTranslations() {
71
+ const t = await firstValueFrom(this.http.get(`${this.baseUrl}/translations`));
72
+ this.translationsMap.set(t);
73
+ }
74
+ setLanguage(lang) {
75
+ this.currentLang.set(lang);
76
+ currentLanguage.set(lang);
77
+ localStorage.setItem("spark-lang", lang);
78
+ }
79
+ resolve(ts) {
80
+ if (!ts) return "";
81
+ return ts[this.currentLang()] ?? ts["en"] ?? Object.values(ts)[0] ?? "";
82
+ }
83
+ t(key) {
84
+ const ts = this.translationsMap()[key];
85
+ return this.resolve(ts) || key;
86
+ }
87
+ static ɵfac = i0.ɵɵngDeclareFactory({
88
+ minVersion: "12.0.0",
89
+ version: "22.2.0",
90
+ ngImport: i0,
91
+ type: SparkLanguageService,
92
+ deps: [],
93
+ target: i0.ɵɵFactoryTarget.Injectable
94
+ });
95
+ static ɵprov = i0.ɵɵngDeclareInjectable({
96
+ minVersion: "12.0.0",
97
+ version: "22.2.0",
98
+ ngImport: i0,
99
+ type: SparkLanguageService,
100
+ providedIn: "root"
101
+ });
102
+ };
103
+ i0.ɵɵngDeclareClassMetadata({
104
+ minVersion: "12.0.0",
105
+ version: "22.2.0",
106
+ ngImport: i0,
107
+ type: SparkLanguageService,
108
+ decorators: [{
109
+ type: Injectable,
110
+ args: [{ providedIn: "root" }]
111
+ }],
112
+ ctorParameters: () => []
113
+ });
114
+ var SparkStreamingService = class SparkStreamingService {
115
+ config = inject(SPARK_CONFIG, { optional: true });
116
+ baseUrl = this.config?.baseUrl ?? "/spark";
117
+ ngZone = inject(NgZone);
118
+ connectToStreamingQuery(queryId) {
119
+ return new Observable((subscriber) => {
120
+ let ws = null;
121
+ let retryCount = 0;
122
+ let retryTimeout = null;
123
+ let closed = false;
124
+ const maxRetries = 10;
125
+ const maxDelay = 3e4;
126
+ const connect = () => {
127
+ if (closed) return;
128
+ const url = this.buildWebSocketUrl(queryId);
129
+ ws = new WebSocket(url);
130
+ ws.onopen = () => {
131
+ retryCount = 0;
132
+ };
133
+ ws.onmessage = (event) => {
134
+ try {
135
+ const message = JSON.parse(event.data);
136
+ this.ngZone.run(() => subscriber.next(message));
137
+ } catch {}
138
+ };
139
+ ws.onerror = () => {};
140
+ ws.onclose = (event) => {
141
+ if (closed) return;
142
+ if (event.code === 1e3) {
143
+ this.ngZone.run(() => subscriber.complete());
144
+ return;
145
+ }
146
+ if (retryCount < maxRetries) {
147
+ const delay = Math.min(1e3 * Math.pow(2, retryCount), maxDelay);
148
+ retryCount++;
149
+ retryTimeout = setTimeout(() => connect(), delay);
150
+ } else this.ngZone.run(() => subscriber.error(/* @__PURE__ */ new Error("WebSocket connection failed after maximum retries")));
151
+ };
152
+ };
153
+ connect();
154
+ return () => {
155
+ closed = true;
156
+ if (retryTimeout) clearTimeout(retryTimeout);
157
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) ws.close(1e3, "Client unsubscribed");
158
+ };
159
+ });
160
+ }
161
+ buildWebSocketUrl(queryId) {
162
+ const encodedId = encodeURIComponent(queryId);
163
+ const path = `${this.baseUrl}/queries/${encodedId}/stream`;
164
+ if (this.baseUrl.startsWith("http://")) return path.replace(/^http:\/\//, "ws://");
165
+ if (this.baseUrl.startsWith("https://")) return path.replace(/^https:\/\//, "wss://");
166
+ return `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}${path}`;
167
+ }
168
+ static ɵfac = i0.ɵɵngDeclareFactory({
169
+ minVersion: "12.0.0",
170
+ version: "22.2.0",
171
+ ngImport: i0,
172
+ type: SparkStreamingService,
173
+ deps: [],
174
+ target: i0.ɵɵFactoryTarget.Injectable
175
+ });
176
+ static ɵprov = i0.ɵɵngDeclareInjectable({
177
+ minVersion: "12.0.0",
178
+ version: "22.2.0",
179
+ ngImport: i0,
180
+ type: SparkStreamingService,
181
+ providedIn: "root"
182
+ });
183
+ };
184
+ i0.ɵɵngDeclareClassMetadata({
185
+ minVersion: "12.0.0",
186
+ version: "22.2.0",
187
+ ngImport: i0,
188
+ type: SparkStreamingService,
189
+ decorators: [{
190
+ type: Injectable,
191
+ args: [{ providedIn: "root" }]
192
+ }]
193
+ });
194
+ var SparkService = class SparkService {
195
+ config = inject(SPARK_CONFIG, { optional: true });
196
+ baseUrl = this.config?.baseUrl ?? "/spark";
197
+ http = inject(HttpClient);
198
+ retryActionService = inject(RetryActionService);
199
+ static MAX_RETRY_DEPTH = 16;
200
+ dispatcher = inject(SparkClientOperationDispatcher);
201
+ async getEntityTypes() {
202
+ return firstValueFrom(this.http.get(`${this.baseUrl}/types`));
203
+ }
204
+ async getEntityType(id) {
205
+ return firstValueFrom(this.http.get(`${this.baseUrl}/types/${encodeURIComponent(id)}`));
206
+ }
207
+ async getEntityTypeByClrType(clrType) {
208
+ return (await this.getEntityTypes()).find((t) => t.clrType === clrType);
209
+ }
210
+ async getPermissions(entityTypeId) {
211
+ return firstValueFrom(this.http.get(`${this.baseUrl}/permissions/${encodeURIComponent(entityTypeId)}`));
212
+ }
213
+ #queryCatalogue;
214
+ async getQueries() {
215
+ return firstValueFrom(this.http.get(`${this.baseUrl}/queries`));
216
+ }
217
+ async getQuery(id) {
218
+ return firstValueFrom(this.http.post(`${this.baseUrl}/queries/get`, { queryId: id }));
219
+ }
220
+ async getQueryByName(name) {
221
+ this.#queryCatalogue ??= this.getQueries();
222
+ return (await this.#queryCatalogue).find((q) => q.name === name);
223
+ }
224
+ async executeQuery(queryId, options) {
225
+ return this.sendRead(`${this.baseUrl}/queries/execute`, {
226
+ queryId,
227
+ sortColumns: options?.sortColumns?.length ? options.sortColumns.map((c) => ({
228
+ property: c.property,
229
+ direction: c.direction === "descending" ? "desc" : "asc"
230
+ })) : void 0,
231
+ parentId: options?.parentId,
232
+ parentType: options?.parentType,
233
+ skip: options?.skip,
234
+ take: options?.take,
235
+ search: options?.search,
236
+ columns: options?.columns?.length ? options.columns : void 0,
237
+ deleted: options?.deleted,
238
+ parentDeleted: options?.parentDeleted
239
+ });
240
+ }
241
+ async getDistinctValues(queryId, column, options) {
242
+ return this.sendRead(`${this.baseUrl}/queries/distinct-values`, {
243
+ queryId,
244
+ column,
245
+ search: options?.search || void 0,
246
+ querySearch: options?.querySearch || void 0,
247
+ columns: options?.columns?.length ? options.columns : void 0,
248
+ deleted: options?.deleted,
249
+ parentId: options?.parentId,
250
+ parentType: options?.parentType,
251
+ parentDeleted: options?.parentDeleted
252
+ });
253
+ }
254
+ async executeQueryByName(queryName, options) {
255
+ const query = await this.getQueryByName(queryName);
256
+ if (!query) return {
257
+ columns: [],
258
+ items: [],
259
+ totalItems: 0,
260
+ skip: 0,
261
+ take: options?.take ?? 50
262
+ };
263
+ return this.executeQuery(query.id, options);
264
+ }
265
+ async getProgramUnits() {
266
+ return firstValueFrom(this.http.get(`${this.baseUrl}/program-units`));
267
+ }
268
+ async get(type, id, options) {
269
+ return this.sendRead(`${this.baseUrl}/po/load`, {
270
+ objectTypeId: type,
271
+ id,
272
+ deleted: options?.deleted
273
+ });
274
+ }
275
+ async create(type, data) {
276
+ return this.postWithEnvelope(`${this.baseUrl}/po/create`, {
277
+ objectTypeId: type,
278
+ persistentObject: data
279
+ });
280
+ }
281
+ async update(type, id, data) {
282
+ return this.postWithEnvelope(`${this.baseUrl}/po/update`, {
283
+ objectTypeId: type,
284
+ id,
285
+ persistentObject: data
286
+ });
287
+ }
288
+ async refresh(type, data, triggeredBy) {
289
+ return this.postWithEnvelope(`${this.baseUrl}/po/refresh`, {
290
+ objectTypeId: type,
291
+ persistentObject: data,
292
+ triggeredBy
293
+ });
294
+ }
295
+ async newObject(type, options) {
296
+ return this.postWithEnvelope(`${this.baseUrl}/po/new`, {
297
+ objectTypeId: type,
298
+ ...options ?? {}
299
+ });
300
+ }
301
+ async deleteRow(type, options) {
302
+ await this.postWithEnvelope(`${this.baseUrl}/po/delete-row`, {
303
+ objectTypeId: type,
304
+ ...options
305
+ });
306
+ }
307
+ async delete(type, id) {
308
+ return this.postWithEnvelope(`${this.baseUrl}/po/delete`, {
309
+ objectTypeId: type,
310
+ id
311
+ });
312
+ }
313
+ async deleteMany(type, ids, options) {
314
+ return this.postWithEnvelope(`${this.baseUrl}/po/delete-many`, {
315
+ objectTypeId: type,
316
+ ids,
317
+ ...options ?? {}
318
+ });
319
+ }
320
+ async getCustomActions(objectTypeId) {
321
+ return firstValueFrom(this.http.post(`${this.baseUrl}/actions/list`, { objectTypeId }));
322
+ }
323
+ async executeCustomAction(objectTypeId, actionName, parent, selectedItemIds, queryParent, queryId) {
324
+ const body = {
325
+ objectTypeId,
326
+ actionName,
327
+ parent,
328
+ selectedItemIds,
329
+ parentId: queryParent?.id,
330
+ parentType: queryParent?.type,
331
+ queryId
332
+ };
333
+ return await this.postWithEnvelope(`${this.baseUrl}/actions/execute`, body) ?? void 0;
334
+ }
335
+ async getLookupReferences() {
336
+ return firstValueFrom(this.http.get(`${this.baseUrl}/lookupref`));
337
+ }
338
+ async getLookupReference(name) {
339
+ return firstValueFrom(this.http.get(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`));
340
+ }
341
+ async addLookupReferenceValue(name, value) {
342
+ return firstValueFrom(this.http.post(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}`, value));
343
+ }
344
+ async updateLookupReferenceValue(name, key, value) {
345
+ return firstValueFrom(this.http.put(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`, value));
346
+ }
347
+ async deleteLookupReferenceValue(name, key) {
348
+ return firstValueFrom(this.http.delete(`${this.baseUrl}/lookupref/${encodeURIComponent(name)}/${encodeURIComponent(key)}`));
349
+ }
350
+ postEnvelope(path, body) {
351
+ return this.postWithEnvelope(`${this.baseUrl}${path}`, body);
352
+ }
353
+ postWithEnvelope(url, body) {
354
+ return this.sendWithEnvelope(() => firstValueFrom(this.http.post(url, body)), body, () => this.postWithEnvelope(url, body));
355
+ }
356
+ async sendRead(url, body) {
357
+ try {
358
+ return await firstValueFrom(this.http.post(url, body));
359
+ } catch (error) {
360
+ return this.handleEnvelopeRetryError(error, () => this.sendRead(url, body), body);
361
+ }
362
+ }
363
+ async sendWithEnvelope(send, body, retryFn) {
364
+ try {
365
+ const envelope = await send();
366
+ if (envelope?.operations?.length) this.dispatcher.dispatch(envelope.operations);
367
+ return envelope?.result;
368
+ } catch (error) {
369
+ return this.handleEnvelopeRetryError(error, retryFn, body);
370
+ }
371
+ }
372
+ async handleEnvelopeRetryError(error, retryFn, body) {
373
+ if (error.status !== 449) throw error;
374
+ const envelope = error.error;
375
+ if (!envelope?.operations?.length) throw error;
376
+ const nonRetry = envelope.operations.filter((o) => o.type !== "retry");
377
+ if (nonRetry.length) this.dispatcher.dispatch(nonRetry);
378
+ const retryOp = envelope.operations.find((o) => o.type === "retry");
379
+ if (!retryOp) throw error;
380
+ if ((body.retryResults?.length ?? 0) >= SparkService.MAX_RETRY_DEPTH) throw new Error(`Gave up after answering ${SparkService.MAX_RETRY_DEPTH} retry prompts; the last was step ${retryOp.step} ("${retryOp.title}"). A hook that raises a retry without checking Retry.Result first will do this — it re-raises on every resubmission.`);
381
+ const payload = {
382
+ type: "retry-action",
383
+ step: retryOp.step,
384
+ title: retryOp.title,
385
+ options: retryOp.options,
386
+ defaultOption: retryOp.defaultOption ?? void 0,
387
+ persistentObject: retryOp.persistentObject ?? void 0,
388
+ message: retryOp.message ?? void 0
389
+ };
390
+ const result = await this.retryActionService.show(payload);
391
+ if (result.option === "Cancel" && !payload.options.includes("Cancel")) throw error;
392
+ body.retryResults = [...body.retryResults || [], result];
393
+ return retryFn();
394
+ }
395
+ static ɵfac = i0.ɵɵngDeclareFactory({
396
+ minVersion: "12.0.0",
397
+ version: "22.2.0",
398
+ ngImport: i0,
399
+ type: SparkService,
400
+ deps: [],
401
+ target: i0.ɵɵFactoryTarget.Injectable
402
+ });
403
+ static ɵprov = i0.ɵɵngDeclareInjectable({
404
+ minVersion: "12.0.0",
405
+ version: "22.2.0",
406
+ ngImport: i0,
407
+ type: SparkService,
408
+ providedIn: "root"
409
+ });
410
+ };
411
+ i0.ɵɵngDeclareClassMetadata({
412
+ minVersion: "12.0.0",
413
+ version: "22.2.0",
414
+ ngImport: i0,
415
+ type: SparkService,
416
+ decorators: [{
417
+ type: Injectable,
418
+ args: [{ providedIn: "root" }]
419
+ }]
420
+ });
451
421
  const SPARK_BUILT_IN_ICONS = {
452
- '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>',
453
- '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>',
454
- '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>',
455
- '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>',
456
- '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>',
457
- '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>',
458
- '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>',
422
+ "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>",
423
+ "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>",
424
+ "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>",
425
+ "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>",
426
+ "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>",
427
+ "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>",
428
+ "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>",
429
+ "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>",
430
+ "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>"
459
431
  };
460
-
461
- /**
462
- * R2-M12: SVG icon registry. The registry no longer accepts a raw string —
463
- * apps must pre-sanitize via `DomSanitizer.bypassSecurityTrustHtml` (or
464
- * `parseSvg` to validate) before calling `register`. Treating the parameter
465
- * as `SafeHtml` puts the explicit bypass decision in the caller's hands so a
466
- * developer wiring server-supplied SVG (per-tenant branding fetched from a
467
- * JSON endpoint) doesn't accidentally trust a `<svg><script>...</script>`
468
- * payload.
469
- *
470
- * The built-in icons baked into the package are still bypass-trusted internally
471
- * because they're known-safe at build time.
472
- */
473
- class SparkIconRegistry {
474
- sanitizer = inject(DomSanitizer);
475
- icons = new Map();
476
- constructor() {
477
- for (const [name, svg] of Object.entries(SPARK_BUILT_IN_ICONS)) {
478
- this.icons.set(name, this.sanitizer.bypassSecurityTrustHtml(svg));
479
- }
480
- }
481
- /**
482
- * Registers a pre-sanitized SVG icon. Callers MUST validate or explicitly
483
- * trust the SVG before passing — for example:
484
- *
485
- * ```ts
486
- * const safe = sanitizer.bypassSecurityTrustHtml(svgFromBuildTimeConstant);
487
- * registry.register('app-logo', safe);
488
- * ```
489
- *
490
- * For server-supplied SVG, prefer a strict allow-list parser (no `<script>`,
491
- * no `on*` attributes, no `href="javascript:..."`) before trusting.
492
- */
493
- register(name, svg) {
494
- this.icons.set(name, svg);
495
- }
496
- get(name) {
497
- return this.icons.get(name);
498
- }
499
- has(name) {
500
- return this.icons.has(name);
501
- }
502
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkIconRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
503
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkIconRegistry, providedIn: 'root' });
432
+ var SparkIconRegistry = class SparkIconRegistry {
433
+ sanitizer = inject(DomSanitizer);
434
+ icons = /* @__PURE__ */ new Map();
435
+ constructor() {
436
+ for (const [name, svg] of Object.entries(SPARK_BUILT_IN_ICONS)) this.icons.set(name, this.sanitizer.bypassSecurityTrustHtml(svg));
437
+ }
438
+ register(name, svg) {
439
+ this.icons.set(name, svg);
440
+ }
441
+ get(name) {
442
+ return this.icons.get(name);
443
+ }
444
+ has(name) {
445
+ return this.icons.has(name);
446
+ }
447
+ static ɵfac = i0.ɵɵngDeclareFactory({
448
+ minVersion: "12.0.0",
449
+ version: "22.2.0",
450
+ ngImport: i0,
451
+ type: SparkIconRegistry,
452
+ deps: [],
453
+ target: i0.ɵɵFactoryTarget.Injectable
454
+ });
455
+ static ɵprov = i0.ɵɵngDeclareInjectable({
456
+ minVersion: "12.0.0",
457
+ version: "22.2.0",
458
+ ngImport: i0,
459
+ type: SparkIconRegistry,
460
+ providedIn: "root"
461
+ });
462
+ };
463
+ i0.ɵɵngDeclareClassMetadata({
464
+ minVersion: "12.0.0",
465
+ version: "22.2.0",
466
+ ngImport: i0,
467
+ type: SparkIconRegistry,
468
+ decorators: [{
469
+ type: Injectable,
470
+ args: [{ providedIn: "root" }]
471
+ }],
472
+ ctorParameters: () => []
473
+ });
474
+ var SparkQueryActionsService = class SparkQueryActionsService {
475
+ sparkService = inject(SparkService);
476
+ async actionsFor(queryIdOrAlias) {
477
+ const context = await this.contextFor(queryIdOrAlias);
478
+ if (!context) return [];
479
+ return filterQueryActions(await this.sparkService.getCustomActions(context.entityType.id));
480
+ }
481
+ async execute(queryIdOrAlias, actionName, options) {
482
+ const context = await this.contextFor(queryIdOrAlias);
483
+ if (!context) throw new Error(`Cannot execute '${actionName}': query '${queryIdOrAlias}' resolves to no entity type, so there is nothing to execute it against.`);
484
+ await this.sparkService.executeCustomAction(context.entityType.id, actionName, options?.parent, options?.selectedItemIds, options?.queryParent, context.query.id);
485
+ }
486
+ async contextFor(queryIdOrAlias) {
487
+ const [query, entityTypes] = await Promise.all([this.sparkService.getQuery(queryIdOrAlias), this.sparkService.getEntityTypes()]);
488
+ if (!query) return null;
489
+ const entityType = entityTypes.find((t) => t.name === query.entityType) ?? entityTypes.find((t) => t.id === query.entityType);
490
+ return entityType ? {
491
+ query,
492
+ entityType
493
+ } : null;
494
+ }
495
+ static ɵfac = i0.ɵɵngDeclareFactory({
496
+ minVersion: "12.0.0",
497
+ version: "22.2.0",
498
+ ngImport: i0,
499
+ type: SparkQueryActionsService,
500
+ deps: [],
501
+ target: i0.ɵɵFactoryTarget.Injectable
502
+ });
503
+ static ɵprov = i0.ɵɵngDeclareInjectable({
504
+ minVersion: "12.0.0",
505
+ version: "22.2.0",
506
+ ngImport: i0,
507
+ type: SparkQueryActionsService,
508
+ providedIn: "root"
509
+ });
510
+ };
511
+ i0.ɵɵngDeclareClassMetadata({
512
+ minVersion: "12.0.0",
513
+ version: "22.2.0",
514
+ ngImport: i0,
515
+ type: SparkQueryActionsService,
516
+ decorators: [{
517
+ type: Injectable,
518
+ args: [{ providedIn: "root" }]
519
+ }]
520
+ });
521
+ const SPARK_TIMEZONE_HEADER = "X-Spark-Timezone";
522
+ const SPARK_TIMEZONE_COOKIE = "spark-timezone";
523
+ const COOKIE_MAX_AGE_SECONDS = 31536e3;
524
+ const ZONE_ID_SHAPE = /^[A-Za-z][A-Za-z0-9_+\-]*(\/[A-Za-z0-9_+\-]+){0,2}$/;
525
+ function resolveBrowserTimeZone() {
526
+ try {
527
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
528
+ } catch {
529
+ return null;
530
+ }
504
531
  }
505
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkIconRegistry, decorators: [{
506
- type: Injectable,
507
- args: [{ providedIn: 'root' }]
508
- }], ctorParameters: () => [] });
509
-
510
- /**
511
- * A query's custom actions, reachable **without the grid that usually renders them**.
512
- *
513
- * ## Why this exists
514
- *
515
- * A query's actions were only ever obtainable by rendering `<spark-query-grid>`, which loads the
516
- * query, resolves its entity type, fetches the actions and filters them to the ones marked for a
517
- * query surface — all privately. A page that wanted the same buttons somewhere else (a toolbar
518
- * above the card, the shell's topbar) had three bad options: duplicate the four-step resolution
519
- * and let it drift, reach into the grid's internals, or put the button in the wrong place.
520
- *
521
- * This exposes the resolution itself. The grid keeps rendering the default placement; a host that
522
- * wants a different one asks here and renders its own control.
523
- *
524
- * ## What it does not do
525
- *
526
- * It does not authorize. `/spark/actions/{type}` returns only the actions this caller may see —
527
- * the server filters against `security.json`, and executing re-checks — so nothing here is a gate,
528
- * and a host must not treat "the list came back empty" as anything other than a display fact.
529
- *
530
- * It also does not cache. Actions depend on the caller, and a memo keyed by type id would survive
531
- * a sign-out.
532
- */
533
- class SparkQueryActionsService {
534
- sparkService = inject(SparkService);
535
- /**
536
- * The custom actions of the query named by id or alias, already filtered to those that belong on
537
- * a query surface.
538
- *
539
- * Returns an empty list — rather than throwing — when the query resolves to no entity type,
540
- * because a caller rendering a toolbar wants "no buttons", not a broken page.
541
- */
542
- async actionsFor(queryIdOrAlias) {
543
- const context = await this.contextFor(queryIdOrAlias);
544
- if (!context)
545
- return [];
546
- // filterQueryActions, not a local predicate: the grid uses it too, and the `showedOn` values
547
- // are exactly the kind of thing a second copy gets subtly wrong — both grids once tested for
548
- // "list", a value nothing emits, and every correctly authored action rendered nowhere.
549
- return filterQueryActions(await this.sparkService.getCustomActions(context.entityType.id));
550
- }
551
- /**
552
- * Runs one of those actions. `selectedItemIds` are row ids, exactly as the grid posts them —
553
- * the server re-materializes each one through the same load path a detail page uses, so an id
554
- * from anywhere is treated as caller input rather than as a verified row.
555
- */
556
- async execute(queryIdOrAlias, actionName, options) {
557
- const context = await this.contextFor(queryIdOrAlias);
558
- if (!context) {
559
- throw new Error(`Cannot execute '${actionName}': query '${queryIdOrAlias}' resolves to no entity type, so there ` +
560
- `is nothing to execute it against.`);
561
- }
562
- await this.sparkService.executeCustomAction(context.entityType.id, actionName, options?.parent, options?.selectedItemIds, options?.queryParent, context.query.id);
563
- }
564
- /**
565
- * The query and the entity type its rows are mapped against — the two-step resolution both
566
- * methods need, kept in one place so they cannot disagree about which type an action runs on.
567
- */
568
- async contextFor(queryIdOrAlias) {
569
- const [query, entityTypes] = await Promise.all([
570
- this.sparkService.getQuery(queryIdOrAlias),
571
- this.sparkService.getEntityTypes(),
572
- ]);
573
- if (!query)
574
- return null;
575
- const entityType = entityTypes.find(t => t.name === query.entityType)
576
- ?? entityTypes.find(t => t.id === query.entityType);
577
- return entityType ? { query, entityType } : null;
578
- }
579
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryActionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
580
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryActionsService, providedIn: 'root' });
532
+ function readCookie(cookies, name) {
533
+ for (const part of cookies.split(";")) {
534
+ const eq = part.indexOf("=");
535
+ if (eq > 0 && part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();
536
+ }
537
+ return null;
581
538
  }
582
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryActionsService, decorators: [{
583
- type: Injectable,
584
- args: [{ providedIn: 'root' }]
585
- }] });
586
-
587
- /**
588
- * The header carrying the viewer's IANA timezone id, e.g. `Europe/Brussels`.
589
- * Must stay in lockstep with `RequestTimeZoneResolver.HeaderName` on the server.
590
- */
591
- const SPARK_TIMEZONE_HEADER = 'X-Spark-Timezone';
592
- /**
593
- * Returns the browser's IANA zone id, or `null` when the environment cannot name one.
594
- *
595
- * An id is deliberately sent rather than a numeric offset: an offset is only valid at one instant,
596
- * so a server asked to render "next Tuesday at 09:00" from an offset captured today gets it wrong
597
- * across a DST boundary. A zone id carries the rules, not one sample of them.
598
- */
599
- function resolveBrowserTimeZone() {
600
- try {
601
- return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
602
- }
603
- catch {
604
- return null;
605
- }
539
+ function writeTimezoneCookie(doc, name, zone) {
540
+ if (readCookie(doc.cookie, name) === zone) return;
541
+ const secure = doc.location?.protocol === "https:" ? "; Secure" : "";
542
+ doc.cookie = `${name}=${zone}; Path=/; Max-Age=${COOKIE_MAX_AGE_SECONDS}; SameSite=Lax${secure}`;
606
543
  }
607
- /**
608
- * Attaches {@link SPARK_TIMEZONE_HEADER} to same-origin requests.
609
- *
610
- * **This does not participate in reading or writing timestamps.** A `DateTimeOffset` means an
611
- * instant, and the browser converts in both directions on those paths. The header exists for work
612
- * the server starts on its own — a scheduled export, a notification email — where there is no
613
- * browser in the loop to do the converting but the output still has to read as local time.
614
- */
615
- const sparkTimezoneInterceptor = (req, next) => {
616
- // Never annotate a cross-origin call: the viewer's timezone is a (small) fingerprinting signal and
617
- // third-party hosts have no business receiving it.
618
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(req.url)) {
619
- try {
620
- if (new URL(req.url).origin !== window.location.origin)
621
- return next(req);
622
- }
623
- catch {
624
- return next(req);
625
- }
626
- }
627
- const zone = resolveBrowserTimeZone();
628
- if (!zone)
629
- return next(req);
630
- return next(req.clone({ setHeaders: { [SPARK_TIMEZONE_HEADER]: zone } }));
631
- };
632
- /**
633
- * `provideHttpClient(...withSparkTimezone())` — registers {@link sparkTimezoneInterceptor}.
634
- *
635
- * Spreadable alongside `withSparkAuth()`:
636
- * `provideHttpClient(...withSparkAuth(), ...withSparkTimezone())`.
637
- */
638
- function withSparkTimezone() {
639
- return [withInterceptors([sparkTimezoneInterceptor])];
544
+ function createSparkTimezoneInterceptor(options = {}) {
545
+ const cookieName = options.cookieName === void 0 ? SPARK_TIMEZONE_COOKIE : options.cookieName;
546
+ return (req, next) => {
547
+ if (!isPlatformBrowser(inject(PLATFORM_ID))) return next(req);
548
+ const zone = resolveBrowserTimeZone();
549
+ if (!zone) return next(req);
550
+ if (cookieName && ZONE_ID_SHAPE.test(zone)) writeTimezoneCookie(inject(DOCUMENT), cookieName, zone);
551
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(req.url)) try {
552
+ if (new URL(req.url).origin !== window.location.origin) return next(req);
553
+ } catch {
554
+ return next(req);
555
+ }
556
+ return next(req.clone({ setHeaders: { [SPARK_TIMEZONE_HEADER]: zone } }));
557
+ };
640
558
  }
559
+ const sparkTimezoneInterceptor = createSparkTimezoneInterceptor();
560
+ function withSparkTimezone(options) {
561
+ return [withInterceptors([options ? createSparkTimezoneInterceptor(options) : sparkTimezoneInterceptor])];
562
+ }
563
+ const SPARK_RETURN_URL_STATE_KEY = "sparkReturnUrl";
564
+ var SparkReturnNavigationService = class SparkReturnNavigationService {
565
+ router = inject(Router);
566
+ spark = inject(SparkService);
567
+ stateForRowLink() {
568
+ return { [SPARK_RETURN_URL_STATE_KEY]: this.router.url };
569
+ }
570
+ recordedReturnUrl() {
571
+ const url = (typeof history !== "undefined" ? history.state : null)?.[SPARK_RETURN_URL_STATE_KEY];
572
+ return typeof url === "string" && url.startsWith("/") && !url.startsWith("//") ? url : null;
573
+ }
574
+ async returnToList(entityTypeName, options) {
575
+ const recorded = this.recordedReturnUrl();
576
+ if (recorded) {
577
+ await this.router.navigateByUrl(recorded, { replaceUrl: true });
578
+ return;
579
+ }
580
+ const route = entityTypeName ? await this.listRouteFor(entityTypeName) : null;
581
+ if (route) {
582
+ await this.router.navigate(route, {
583
+ replaceUrl: true,
584
+ ...options?.deletedView ? { queryParams: { deleted: "only" } } : {}
585
+ });
586
+ return;
587
+ }
588
+ await this.router.navigate(["/"], { replaceUrl: true });
589
+ }
590
+ async listRouteFor(entityTypeName) {
591
+ let queries;
592
+ try {
593
+ queries = (await this.spark.getQueries()).filter((q) => q.entityType === entityTypeName);
594
+ } catch {
595
+ return null;
596
+ }
597
+ if (!queries.length) return null;
598
+ try {
599
+ const units = (await this.spark.getProgramUnits()).programUnitGroups.flatMap((g) => g.programUnits).filter((u) => u.type === "query").sort((a, b) => a.order - b.order);
600
+ for (const unit of units) if (queries.some((q) => q.id === unit.queryId || !!unit.alias && q.alias === unit.alias)) return ["/query", unit.alias || unit.queryId];
601
+ } catch {}
602
+ return ["/query", queries[0].alias || queries[0].id];
603
+ }
604
+ static ɵfac = i0.ɵɵngDeclareFactory({
605
+ minVersion: "12.0.0",
606
+ version: "22.2.0",
607
+ ngImport: i0,
608
+ type: SparkReturnNavigationService,
609
+ deps: [],
610
+ target: i0.ɵɵFactoryTarget.Injectable
611
+ });
612
+ static ɵprov = i0.ɵɵngDeclareInjectable({
613
+ minVersion: "12.0.0",
614
+ version: "22.2.0",
615
+ ngImport: i0,
616
+ type: SparkReturnNavigationService,
617
+ providedIn: "root"
618
+ });
619
+ };
620
+ i0.ɵɵngDeclareClassMetadata({
621
+ minVersion: "12.0.0",
622
+ version: "22.2.0",
623
+ ngImport: i0,
624
+ type: SparkReturnNavigationService,
625
+ decorators: [{
626
+ type: Injectable,
627
+ args: [{ providedIn: "root" }]
628
+ }]
629
+ });
630
+ export { RetryActionService, SPARK_RETURN_URL_STATE_KEY, SPARK_TIMEZONE_COOKIE, SPARK_TIMEZONE_HEADER, SparkIconRegistry, SparkLanguageService, SparkQueryActionsService, SparkReturnNavigationService, SparkService, SparkStreamingService, createSparkTimezoneInterceptor, resolveBrowserTimeZone, sparkTimezoneInterceptor, withSparkTimezone };
641
631
 
642
- /**
643
- * Generated bundle index. Do not edit.
644
- */
645
-
646
- export { RetryActionService, SPARK_TIMEZONE_HEADER, SparkIconRegistry, SparkLanguageService, SparkQueryActionsService, SparkService, SparkStreamingService, resolveBrowserTimeZone, sparkTimezoneInterceptor, withSparkTimezone };
647
- //# sourceMappingURL=mintplayer-ng-spark-services.mjs.map
632
+ //# sourceMappingURL=mintplayer-ng-spark-services.mjs.map