@mintplayer/ng-spark 22.31.0 → 22.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +101 -1
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +3 -3
  3. package/fesm2022/mintplayer-ng-spark-auth-account.mjs +18 -18
  4. package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs +16 -8
  5. package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs +3 -3
  7. package/fesm2022/mintplayer-ng-spark-auth-core.mjs +6 -6
  8. package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs +3 -3
  9. package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs +3 -3
  10. package/fesm2022/mintplayer-ng-spark-auth-login.mjs +3 -3
  11. package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs +3 -3
  12. package/fesm2022/mintplayer-ng-spark-auth-register.mjs +3 -3
  13. package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs +3 -3
  14. package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs +3 -3
  15. package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs +3 -3
  16. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +24 -24
  17. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +3 -3
  18. package/fesm2022/mintplayer-ng-spark-contributions.mjs +9 -9
  19. package/fesm2022/mintplayer-ng-spark-grid.mjs +220 -49
  20. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  21. package/fesm2022/mintplayer-ng-spark-history.mjs +9 -9
  22. package/fesm2022/mintplayer-ng-spark-icon.mjs +3 -3
  23. package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs +3 -3
  24. package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs +9 -9
  25. package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs +6 -6
  26. package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs +3 -3
  27. package/fesm2022/mintplayer-ng-spark-models.mjs +44 -1
  28. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  29. package/fesm2022/mintplayer-ng-spark-moderation.mjs +21 -21
  30. package/fesm2022/mintplayer-ng-spark-notifications.mjs +687 -0
  31. package/fesm2022/mintplayer-ng-spark-notifications.mjs.map +1 -0
  32. package/fesm2022/mintplayer-ng-spark-organizations-accept.mjs +179 -0
  33. package/fesm2022/mintplayer-ng-spark-organizations-accept.mjs.map +1 -0
  34. package/fesm2022/mintplayer-ng-spark-organizations.mjs +202 -0
  35. package/fesm2022/mintplayer-ng-spark-organizations.mjs.map +1 -0
  36. package/fesm2022/mintplayer-ng-spark-pipes.mjs +125 -85
  37. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-po-create.mjs +3 -3
  39. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +90 -26
  40. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  41. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +84 -12
  42. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  43. package/fesm2022/mintplayer-ng-spark-po-form.mjs +9 -9
  44. package/fesm2022/mintplayer-ng-spark-pwa.mjs +74 -6
  45. package/fesm2022/mintplayer-ng-spark-pwa.mjs.map +1 -1
  46. package/fesm2022/mintplayer-ng-spark-query-list.mjs +10 -109
  47. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  48. package/fesm2022/mintplayer-ng-spark-renderers.mjs +3 -3
  49. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +3 -3
  50. package/fesm2022/mintplayer-ng-spark-score-renderers.mjs +801 -0
  51. package/fesm2022/mintplayer-ng-spark-score-renderers.mjs.map +1 -0
  52. package/fesm2022/mintplayer-ng-spark-services.mjs +424 -72
  53. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  54. package/fesm2022/mintplayer-ng-spark-share-page.mjs +334 -0
  55. package/fesm2022/mintplayer-ng-spark-share-page.mjs.map +1 -0
  56. package/fesm2022/mintplayer-ng-spark-share.mjs +10 -0
  57. package/fesm2022/mintplayer-ng-spark-share.mjs.map +1 -0
  58. package/fesm2022/mintplayer-ng-spark-shell.mjs +38 -36
  59. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  60. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +9 -9
  61. package/package.json +25 -1
  62. package/types/mintplayer-ng-spark-grid.d.ts +77 -26
  63. package/types/mintplayer-ng-spark-models.d.ts +150 -17
  64. package/types/mintplayer-ng-spark-notifications.d.ts +188 -0
  65. package/types/mintplayer-ng-spark-organizations-accept.d.ts +40 -0
  66. package/types/mintplayer-ng-spark-organizations.d.ts +72 -0
  67. package/types/mintplayer-ng-spark-pipes.d.ts +10 -0
  68. package/types/mintplayer-ng-spark-po-detail.d.ts +26 -0
  69. package/types/mintplayer-ng-spark-po-edit.d.ts +37 -0
  70. package/types/mintplayer-ng-spark-pwa.d.ts +48 -2
  71. package/types/mintplayer-ng-spark-query-list.d.ts +8 -23
  72. package/types/mintplayer-ng-spark-score-renderers.d.ts +340 -0
  73. package/types/mintplayer-ng-spark-services.d.ts +146 -6
  74. package/types/mintplayer-ng-spark-share-page.d.ts +67 -0
  75. package/types/mintplayer-ng-spark-share.d.ts +26 -0
@@ -1,9 +1,74 @@
1
1
  import * as i0 from "@angular/core";
2
2
  import { ApplicationRef, DestroyRef, Injectable, InjectionToken, PLATFORM_ID, inject, isDevMode, makeEnvironmentProviders, provideAppInitializer, signal } from "@angular/core";
3
- import { SwUpdate, provideServiceWorker } from "@angular/service-worker";
3
+ import { SwPush, SwUpdate, provideServiceWorker } from "@angular/service-worker";
4
4
  import { DOCUMENT, Location, isPlatformBrowser } from "@angular/common";
5
5
  import { NavigationStart, Router } from "@angular/router";
6
6
  import { Subscription, filter, first } from "rxjs";
7
+ function sparkInAppUrl(url, document, location) {
8
+ if (!url) return null;
9
+ let parsed;
10
+ try {
11
+ parsed = new URL(url, document.baseURI);
12
+ } catch {
13
+ return null;
14
+ }
15
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
16
+ if (parsed.origin !== document.location?.origin) return null;
17
+ const path = location ? location.normalize(parsed.pathname) : parsed.pathname;
18
+ return (path.startsWith("/") ? path : "/" + path) + parsed.search + parsed.hash;
19
+ }
20
+ var SparkNotificationClicks = class SparkNotificationClicks {
21
+ swPush = inject(SwPush);
22
+ router = inject(Router, { optional: true });
23
+ location = inject(Location, { optional: true });
24
+ document = inject(DOCUMENT);
25
+ platformId = inject(PLATFORM_ID);
26
+ destroyRef = inject(DestroyRef);
27
+ started = false;
28
+ start() {
29
+ if (this.started) return;
30
+ this.started = true;
31
+ if (!isPlatformBrowser(this.platformId) || !this.swPush.isEnabled || !this.router) return;
32
+ const subscription = this.swPush.notificationClicks.subscribe(({ notification }) => {
33
+ const data = notification?.data;
34
+ this.navigate(typeof data?.url === "string" ? data.url : null);
35
+ });
36
+ this.destroyRef.onDestroy(() => subscription.unsubscribe());
37
+ }
38
+ navigate(url) {
39
+ const target = sparkInAppUrl(url, this.document, this.location);
40
+ if (!target || !this.router || this.router.url === target) return;
41
+ this.router.navigateByUrl(target).catch(() => this.assign(new URL(url, this.document.baseURI).href));
42
+ }
43
+ assign(href) {
44
+ this.document.defaultView?.location.assign(href);
45
+ }
46
+ static ɵfac = i0.ɵɵngDeclareFactory({
47
+ minVersion: "12.0.0",
48
+ version: "22.2.2",
49
+ ngImport: i0,
50
+ type: SparkNotificationClicks,
51
+ deps: [],
52
+ target: i0.ɵɵFactoryTarget.Injectable
53
+ });
54
+ static ɵprov = i0.ɵɵngDeclareInjectable({
55
+ minVersion: "12.0.0",
56
+ version: "22.2.2",
57
+ ngImport: i0,
58
+ type: SparkNotificationClicks,
59
+ providedIn: "root"
60
+ });
61
+ };
62
+ i0.ɵɵngDeclareClassMetadata({
63
+ minVersion: "12.0.0",
64
+ version: "22.2.2",
65
+ ngImport: i0,
66
+ type: SparkNotificationClicks,
67
+ decorators: [{
68
+ type: Injectable,
69
+ args: [{ providedIn: "root" }]
70
+ }]
71
+ });
7
72
  const SPARK_SW_PAGE_LOADER = new InjectionToken("SPARK_SW_PAGE_LOADER", {
8
73
  providedIn: "root",
9
74
  factory: () => {
@@ -121,7 +186,7 @@ var SparkServiceWorkerUpdates = class SparkServiceWorkerUpdates {
121
186
  }
122
187
  static ɵfac = i0.ɵɵngDeclareFactory({
123
188
  minVersion: "12.0.0",
124
- version: "22.2.0",
189
+ version: "22.2.2",
125
190
  ngImport: i0,
126
191
  type: SparkServiceWorkerUpdates,
127
192
  deps: [],
@@ -129,7 +194,7 @@ var SparkServiceWorkerUpdates = class SparkServiceWorkerUpdates {
129
194
  });
130
195
  static ɵprov = i0.ɵɵngDeclareInjectable({
131
196
  minVersion: "12.0.0",
132
- version: "22.2.0",
197
+ version: "22.2.2",
133
198
  ngImport: i0,
134
199
  type: SparkServiceWorkerUpdates,
135
200
  providedIn: "root"
@@ -137,7 +202,7 @@ var SparkServiceWorkerUpdates = class SparkServiceWorkerUpdates {
137
202
  };
138
203
  i0.ɵɵngDeclareClassMetadata({
139
204
  minVersion: "12.0.0",
140
- version: "22.2.0",
205
+ version: "22.2.2",
141
206
  ngImport: i0,
142
207
  type: SparkServiceWorkerUpdates,
143
208
  decorators: [{
@@ -155,8 +220,11 @@ function provideSparkServiceWorker(options = {}) {
155
220
  return makeEnvironmentProviders([provideServiceWorker(options.script ?? "ngsw-worker.js", {
156
221
  enabled: options.enabled ?? !isDevMode(),
157
222
  registrationStrategy: options.registrationStrategy ?? "registerWhenStable:30000"
158
- }), provideAppInitializer(() => inject(SparkServiceWorkerUpdates).start())]);
223
+ }), provideAppInitializer(() => {
224
+ inject(SparkServiceWorkerUpdates).start();
225
+ inject(SparkNotificationClicks).start();
226
+ })]);
159
227
  }
160
- export { SPARK_SW_PAGE_LOADER, SPARK_SW_SCHEDULE, SparkServiceWorkerUpdates, provideSparkServiceWorker };
228
+ export { SPARK_SW_PAGE_LOADER, SPARK_SW_SCHEDULE, SparkNotificationClicks, SparkServiceWorkerUpdates, provideSparkServiceWorker, sparkInAppUrl };
161
229
 
162
230
  //# sourceMappingURL=mintplayer-ng-spark-pwa.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"mintplayer-ng-spark-pwa.mjs","names":[],"sources":["../../pwa/src/spark-service-worker-updates.ts","../../pwa/src/provide-spark-service-worker.ts"],"sourcesContent":["import { DOCUMENT, Location, isPlatformBrowser } from '@angular/common';\nimport {\n ApplicationRef,\n DestroyRef,\n Injectable,\n InjectionToken,\n PLATFORM_ID,\n inject,\n signal,\n} from '@angular/core';\nimport { NavigationStart, Router } from '@angular/router';\nimport { SwUpdate, VersionReadyEvent } from '@angular/service-worker';\nimport { Subscription, filter, first } from 'rxjs';\n\n/** A service-worker version as `ngsw.json` describes it. `appData` is the config's `appData`. */\nexport interface SparkAppVersion {\n hash: string;\n appData?: Record<string, unknown>;\n}\n\n/** The two page loads the update policy performs. Replaceable for tests. */\nexport interface SparkPageLoader {\n assign(url: string): void;\n reload(): void;\n}\n\n/** How the update policy loads a page: `window.location` by default. */\nexport const SPARK_SW_PAGE_LOADER = new InjectionToken<SparkPageLoader>('SPARK_SW_PAGE_LOADER', {\n providedIn: 'root',\n factory: () => {\n const location = inject(DOCUMENT).defaultView?.location;\n return {\n assign: (url: string) => location?.assign(url),\n reload: () => location?.reload(),\n };\n },\n});\n\n/** Update-check timing. Defaults: every 6 h, and on becoming visible at most once a minute. */\nexport interface SparkServiceWorkerSchedule {\n checkIntervalMs: number;\n visibleCheckThrottleMs: number;\n}\n\nexport const SPARK_SW_SCHEDULE = new InjectionToken<SparkServiceWorkerSchedule>('SPARK_SW_SCHEDULE', {\n providedIn: 'root',\n factory: () => ({ checkIntervalMs: 6 * 60 * 60 * 1000, visibleCheckThrottleMs: 60 * 1000 }),\n});\n\n/**\n * Remembers, per tab, the version a full load was made for. If the page that load produced still\n * reports the same version as ready (another tab keeps the old version alive, or the worker did not\n * hand this client the new one), the policy does not reload again: one attempt per version.\n */\nconst RELOADED_FOR_KEY = 'spark-sw-reloaded-for';\n\n/**\n * The update policy of `provideSparkServiceWorker()` (#464, Q1b).\n *\n * - `VERSION_READY`: the **next** router navigation becomes a full page load of its target, so the\n * user lands on the page they asked for, in the new version, without losing anything on the page\n * they are leaving (the navigation was leaving it anyway).\n * - `unrecoverable`: the running version can no longer be served, so the page reloads at once.\n * - Checks: once the application is stable, every 6 h, and when the tab becomes visible again\n * (at most once a minute).\n * - **A hidden tab never reloads.** Spark has no unsaved-changes tracking to consult, so a reload\n * nobody is looking at could throw away an open edit. An `unrecoverable` in a hidden tab waits\n * until the tab is visible; a navigation in a hidden tab stays a normal in-app navigation.\n *\n * Nothing happens when the service worker is disabled (development builds, unsupported browsers)\n * or on the server.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkServiceWorkerUpdates {\n private readonly swUpdate = inject(SwUpdate);\n private readonly router = inject(Router, { optional: true });\n private readonly location = inject(Location, { optional: true });\n private readonly appRef = inject(ApplicationRef);\n private readonly document = inject(DOCUMENT);\n private readonly platformId = inject(PLATFORM_ID);\n private readonly destroyRef = inject(DestroyRef);\n private readonly pageLoader = inject(SPARK_SW_PAGE_LOADER);\n private readonly schedule = inject(SPARK_SW_SCHEDULE);\n\n private readonly _updateReady = signal(false);\n private readonly _currentVersion = signal<SparkAppVersion | null>(null);\n private readonly _latestVersion = signal<SparkAppVersion | null>(null);\n\n /** True once a new version is downloaded and the next navigation will load it. */\n readonly updateReady = this._updateReady.asReadonly();\n /** The running version, known once an update was reported. */\n readonly currentVersion = this._currentVersion.asReadonly();\n /** The version the next navigation loads (`appData.build` is the ng-spark version). */\n readonly latestVersion = this._latestVersion.asReadonly();\n\n private started = false;\n private leaving = false;\n private reloadWhenVisible = false;\n private lastCheck = 0;\n\n /** Starts the policy. Idempotent; called by the app initializer `provideSparkServiceWorker()` adds. */\n start(): void {\n if (this.started) return;\n this.started = true;\n\n if (!isPlatformBrowser(this.platformId) || !this.swUpdate.isEnabled) return;\n\n const subscriptions = new Subscription();\n this.destroyRef.onDestroy(() => subscriptions.unsubscribe());\n\n subscriptions.add(\n this.swUpdate.versionUpdates\n .pipe(filter((event): event is VersionReadyEvent => event.type === 'VERSION_READY'))\n .subscribe((event) => this.onVersionReady(event)),\n );\n\n subscriptions.add(this.swUpdate.unrecoverable.subscribe(() => this.onUnrecoverable()));\n\n if (this.router) {\n subscriptions.add(\n this.router.events\n .pipe(filter((event): event is NavigationStart => event instanceof NavigationStart))\n .subscribe((event) => this.onNavigationStart(event)),\n );\n }\n\n // The first check waits for stability, so it never competes with the initial render.\n subscriptions.add(\n this.appRef.isStable.pipe(first((stable) => stable)).subscribe(() => {\n this.check();\n const interval = setInterval(() => this.check(), this.schedule.checkIntervalMs);\n subscriptions.add(() => clearInterval(interval));\n }),\n );\n\n const onVisibilityChange = () => {\n if (this.isHidden()) return;\n if (this.reloadWhenVisible) {\n this.reloadWhenVisible = false;\n this.reload();\n return;\n }\n if (Date.now() - this.lastCheck >= this.schedule.visibleCheckThrottleMs) this.check();\n };\n this.document.addEventListener('visibilitychange', onVisibilityChange);\n subscriptions.add(() => this.document.removeEventListener('visibilitychange', onVisibilityChange));\n }\n\n private onVersionReady(event: VersionReadyEvent): void {\n const latest = toVersion(event.latestVersion);\n this._currentVersion.set(toVersion(event.currentVersion));\n this._latestVersion.set(latest);\n\n if (this.readReloadedFor() === latest.hash) {\n // This page is the full load made for that version and it is still not the one served.\n // Reloading again would loop; the next deploy, or the next fresh tab, picks it up.\n return;\n }\n this._updateReady.set(true);\n }\n\n private onUnrecoverable(): void {\n if (this.isHidden()) {\n this.reloadWhenVisible = true;\n return;\n }\n this.reload();\n }\n\n private onNavigationStart(event: NavigationStart): void {\n if (!this._updateReady() || this.leaving || this.isHidden()) return;\n\n this.leaving = true;\n this.writeReloadedFor(this._latestVersion()?.hash);\n this.router?.currentNavigation()?.abort();\n\n if (event.navigationTrigger === 'popstate') {\n // Back/forward: the address bar already shows the target, and assign() would add a\n // duplicate history entry.\n this.pageLoader.reload();\n return;\n }\n\n // event.url is relative to the router's base; prepareExternalUrl adds the <base href>.\n const target = this.location ? this.location.prepareExternalUrl(event.url) : event.url;\n this.pageLoader.assign(target);\n }\n\n private check(): void {\n this.lastCheck = Date.now();\n this.swUpdate.checkForUpdate().catch(() => {\n // Offline, or the worker is gone. The next scheduled check tries again.\n });\n }\n\n private reload(): void {\n if (this.leaving) return;\n this.leaving = true;\n this.pageLoader.reload();\n }\n\n private isHidden(): boolean {\n return this.document.visibilityState === 'hidden';\n }\n\n private readReloadedFor(): string | null {\n try {\n return this.document.defaultView?.sessionStorage.getItem(RELOADED_FOR_KEY) ?? null;\n } catch {\n return null;\n }\n }\n\n private writeReloadedFor(hash: string | undefined): void {\n if (!hash) return;\n try {\n this.document.defaultView?.sessionStorage.setItem(RELOADED_FOR_KEY, hash);\n } catch {\n // Storage blocked: the guard is best effort; one reload per navigation is still bounded.\n }\n }\n}\n\nfunction toVersion(version: { hash: string; appData?: object }): SparkAppVersion {\n return { hash: version.hash, appData: version.appData as Record<string, unknown> | undefined };\n}\n","import { EnvironmentProviders, inject, isDevMode, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';\nimport { SwRegistrationOptions, provideServiceWorker } from '@angular/service-worker';\n\nimport { SparkServiceWorkerUpdates } from './spark-service-worker-updates';\n\nexport interface SparkServiceWorkerOptions {\n /** Whether the worker registers. Default: `!isDevMode()`, so only production builds register it. */\n enabled?: boolean;\n /** The worker script, relative to the base href. Default: `ngsw-worker.js`. */\n script?: string;\n /** When the worker registers. Default: `registerWhenStable:30000`. */\n registrationStrategy?: SwRegistrationOptions['registrationStrategy'];\n}\n\n/**\n * Registers Angular's service worker with Spark's update policy (#464, Q1b); see\n * {@link SparkServiceWorkerUpdates} for what happens when a new version is deployed.\n *\n * The application also needs an `ngsw-config.json` with Spark's navigation exclusions and\n * `serviceWorker` in its production build configuration; see `docs/guide-pwa.md`.\n */\nexport function provideSparkServiceWorker(options: SparkServiceWorkerOptions = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n provideServiceWorker(options.script ?? 'ngsw-worker.js', {\n enabled: options.enabled ?? !isDevMode(),\n registrationStrategy: options.registrationStrategy ?? 'registerWhenStable:30000',\n }),\n provideAppInitializer(() => inject(SparkServiceWorkerUpdates).start()),\n ]);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mintplayer-ng-spark-pwa.mjs","names":[],"sources":["../../pwa/src/spark-in-app-url.ts","../../pwa/src/spark-notification-clicks.ts","../../pwa/src/spark-service-worker-updates.ts","../../pwa/src/provide-spark-service-worker.ts"],"sourcesContent":["import { Location } from '@angular/common';\n\n/**\n * The router URL for `url` when it points into this application, else `null`.\n *\n * `url` may be app-relative (`/runs/42`) or absolute. It is resolved against the document's base URI;\n * only an `http(s)` URL on the page's own origin is in-app, and its path loses the `<base href>`\n * (through `Location.normalize`) because the router works below it. Everything else (another origin,\n * `javascript:`, `mailto:`, an unparseable value) is `null`, so a caller never hands data-borne URLs\n * to the router or to `location.assign` unchecked.\n */\nexport function sparkInAppUrl(url: string | null | undefined, document: Document, location: Location | null): string | null {\n if (!url) return null;\n let parsed: URL;\n try {\n parsed = new URL(url, document.baseURI);\n } catch {\n return null;\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;\n if (parsed.origin !== document.location?.origin) return null;\n const path = location ? location.normalize(parsed.pathname) : parsed.pathname;\n return (path.startsWith('/') ? path : '/' + path) + parsed.search + parsed.hash;\n}\n","import { DOCUMENT, Location, isPlatformBrowser } from '@angular/common';\nimport { DestroyRef, Injectable, PLATFORM_ID, inject } from '@angular/core';\nimport { Router } from '@angular/router';\nimport { SwPush } from '@angular/service-worker';\nimport { sparkInAppUrl } from './spark-in-app-url';\n\n/**\n * Notification clicks while the application is open (`docs/seo_analyzer_PRD.md` X10, D30): a click on\n * a push whose `notification.data.url` is on this origin becomes an in-app router navigation, so the\n * page keeps its state instead of loading again.\n *\n * What it does NOT do, because Angular's service worker (ngsw) already does it: show the push, and\n * open the page when the application is closed. The server sends Angular's payload,\n * `{ notification: { title, body, tag, data: { url, notificationId, onActionClick: { default: {\n * operation, url } } } } }`, and ngsw performs `onActionClick.default` itself on the click, then\n * broadcasts the click to the open pages, which is what {@link SwPush.notificationClicks} reports here.\n *\n * ⚠️ The operation decides whether this handler matters. ngsw runs it BEFORE the broadcast:\n * `navigateLastFocusedOrOpen` makes the focused page load the URL in full, so this handler then\n * finds the router already there and does nothing; `focusLastFocusedOrOpen` focuses the open page\n * without loading it (and opens the URL when no page is open), leaving the navigation to this handler.\n *\n * Active only in a browser with the service worker enabled; started by `provideSparkServiceWorker()`.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkNotificationClicks {\n private readonly swPush = inject(SwPush);\n private readonly router = inject(Router, { optional: true });\n private readonly location = inject(Location, { optional: true });\n private readonly document = inject(DOCUMENT);\n private readonly platformId = inject(PLATFORM_ID);\n private readonly destroyRef = inject(DestroyRef);\n\n private started = false;\n\n /** Starts listening. Idempotent; called by the app initializer `provideSparkServiceWorker()` adds. */\n start(): void {\n if (this.started) return;\n this.started = true;\n if (!isPlatformBrowser(this.platformId) || !this.swPush.isEnabled || !this.router) return;\n\n const subscription = this.swPush.notificationClicks.subscribe(({ notification }) => {\n const data = notification?.data as { url?: unknown } | undefined;\n this.navigate(typeof data?.url === 'string' ? data.url : null);\n });\n this.destroyRef.onDestroy(() => subscription.unsubscribe());\n }\n\n private navigate(url: string | null): void {\n const target = sparkInAppUrl(url, this.document, this.location);\n // Another origin is ngsw's to open (onActionClick); the router cannot reach it.\n if (!target || !this.router || this.router.url === target) return;\n // A same-origin link no route matches is the server's (a PDF download): load it as a page.\n this.router.navigateByUrl(target).catch(() => this.assign(new URL(url!, this.document.baseURI).href));\n }\n\n /** A full page load (overridden in specs: jsdom cannot navigate). */\n protected assign(href: string): void {\n this.document.defaultView?.location.assign(href);\n }\n}\n","import { DOCUMENT, Location, isPlatformBrowser } from '@angular/common';\nimport {\n ApplicationRef,\n DestroyRef,\n Injectable,\n InjectionToken,\n PLATFORM_ID,\n inject,\n signal,\n} from '@angular/core';\nimport { NavigationStart, Router } from '@angular/router';\nimport { SwUpdate, VersionReadyEvent } from '@angular/service-worker';\nimport { Subscription, filter, first } from 'rxjs';\n\n/** A service-worker version as `ngsw.json` describes it. `appData` is the config's `appData`. */\nexport interface SparkAppVersion {\n hash: string;\n appData?: Record<string, unknown>;\n}\n\n/** The two page loads the update policy performs. Replaceable for tests. */\nexport interface SparkPageLoader {\n assign(url: string): void;\n reload(): void;\n}\n\n/** How the update policy loads a page: `window.location` by default. */\nexport const SPARK_SW_PAGE_LOADER = new InjectionToken<SparkPageLoader>('SPARK_SW_PAGE_LOADER', {\n providedIn: 'root',\n factory: () => {\n const location = inject(DOCUMENT).defaultView?.location;\n return {\n assign: (url: string) => location?.assign(url),\n reload: () => location?.reload(),\n };\n },\n});\n\n/** Update-check timing. Defaults: every 6 h, and on becoming visible at most once a minute. */\nexport interface SparkServiceWorkerSchedule {\n checkIntervalMs: number;\n visibleCheckThrottleMs: number;\n}\n\nexport const SPARK_SW_SCHEDULE = new InjectionToken<SparkServiceWorkerSchedule>('SPARK_SW_SCHEDULE', {\n providedIn: 'root',\n factory: () => ({ checkIntervalMs: 6 * 60 * 60 * 1000, visibleCheckThrottleMs: 60 * 1000 }),\n});\n\n/**\n * Remembers, per tab, the version a full load was made for. If the page that load produced still\n * reports the same version as ready (another tab keeps the old version alive, or the worker did not\n * hand this client the new one), the policy does not reload again: one attempt per version.\n */\nconst RELOADED_FOR_KEY = 'spark-sw-reloaded-for';\n\n/**\n * The update policy of `provideSparkServiceWorker()` (#464, Q1b).\n *\n * - `VERSION_READY`: the **next** router navigation becomes a full page load of its target, so the\n * user lands on the page they asked for, in the new version, without losing anything on the page\n * they are leaving (the navigation was leaving it anyway).\n * - `unrecoverable`: the running version can no longer be served, so the page reloads at once.\n * - Checks: once the application is stable, every 6 h, and when the tab becomes visible again\n * (at most once a minute).\n * - **A hidden tab never reloads.** Spark has no unsaved-changes tracking to consult, so a reload\n * nobody is looking at could throw away an open edit. An `unrecoverable` in a hidden tab waits\n * until the tab is visible; a navigation in a hidden tab stays a normal in-app navigation.\n *\n * Nothing happens when the service worker is disabled (development builds, unsupported browsers)\n * or on the server.\n */\n@Injectable({ providedIn: 'root' })\nexport class SparkServiceWorkerUpdates {\n private readonly swUpdate = inject(SwUpdate);\n private readonly router = inject(Router, { optional: true });\n private readonly location = inject(Location, { optional: true });\n private readonly appRef = inject(ApplicationRef);\n private readonly document = inject(DOCUMENT);\n private readonly platformId = inject(PLATFORM_ID);\n private readonly destroyRef = inject(DestroyRef);\n private readonly pageLoader = inject(SPARK_SW_PAGE_LOADER);\n private readonly schedule = inject(SPARK_SW_SCHEDULE);\n\n private readonly _updateReady = signal(false);\n private readonly _currentVersion = signal<SparkAppVersion | null>(null);\n private readonly _latestVersion = signal<SparkAppVersion | null>(null);\n\n /** True once a new version is downloaded and the next navigation will load it. */\n readonly updateReady = this._updateReady.asReadonly();\n /** The running version, known once an update was reported. */\n readonly currentVersion = this._currentVersion.asReadonly();\n /** The version the next navigation loads (`appData.build` is the ng-spark version). */\n readonly latestVersion = this._latestVersion.asReadonly();\n\n private started = false;\n private leaving = false;\n private reloadWhenVisible = false;\n private lastCheck = 0;\n\n /** Starts the policy. Idempotent; called by the app initializer `provideSparkServiceWorker()` adds. */\n start(): void {\n if (this.started) return;\n this.started = true;\n\n if (!isPlatformBrowser(this.platformId) || !this.swUpdate.isEnabled) return;\n\n const subscriptions = new Subscription();\n this.destroyRef.onDestroy(() => subscriptions.unsubscribe());\n\n subscriptions.add(\n this.swUpdate.versionUpdates\n .pipe(filter((event): event is VersionReadyEvent => event.type === 'VERSION_READY'))\n .subscribe((event) => this.onVersionReady(event)),\n );\n\n subscriptions.add(this.swUpdate.unrecoverable.subscribe(() => this.onUnrecoverable()));\n\n if (this.router) {\n subscriptions.add(\n this.router.events\n .pipe(filter((event): event is NavigationStart => event instanceof NavigationStart))\n .subscribe((event) => this.onNavigationStart(event)),\n );\n }\n\n // The first check waits for stability, so it never competes with the initial render.\n subscriptions.add(\n this.appRef.isStable.pipe(first((stable) => stable)).subscribe(() => {\n this.check();\n const interval = setInterval(() => this.check(), this.schedule.checkIntervalMs);\n subscriptions.add(() => clearInterval(interval));\n }),\n );\n\n const onVisibilityChange = () => {\n if (this.isHidden()) return;\n if (this.reloadWhenVisible) {\n this.reloadWhenVisible = false;\n this.reload();\n return;\n }\n if (Date.now() - this.lastCheck >= this.schedule.visibleCheckThrottleMs) this.check();\n };\n this.document.addEventListener('visibilitychange', onVisibilityChange);\n subscriptions.add(() => this.document.removeEventListener('visibilitychange', onVisibilityChange));\n }\n\n private onVersionReady(event: VersionReadyEvent): void {\n const latest = toVersion(event.latestVersion);\n this._currentVersion.set(toVersion(event.currentVersion));\n this._latestVersion.set(latest);\n\n if (this.readReloadedFor() === latest.hash) {\n // This page is the full load made for that version and it is still not the one served.\n // Reloading again would loop; the next deploy, or the next fresh tab, picks it up.\n return;\n }\n this._updateReady.set(true);\n }\n\n private onUnrecoverable(): void {\n if (this.isHidden()) {\n this.reloadWhenVisible = true;\n return;\n }\n this.reload();\n }\n\n private onNavigationStart(event: NavigationStart): void {\n if (!this._updateReady() || this.leaving || this.isHidden()) return;\n\n this.leaving = true;\n this.writeReloadedFor(this._latestVersion()?.hash);\n this.router?.currentNavigation()?.abort();\n\n if (event.navigationTrigger === 'popstate') {\n // Back/forward: the address bar already shows the target, and assign() would add a\n // duplicate history entry.\n this.pageLoader.reload();\n return;\n }\n\n // event.url is relative to the router's base; prepareExternalUrl adds the <base href>.\n const target = this.location ? this.location.prepareExternalUrl(event.url) : event.url;\n this.pageLoader.assign(target);\n }\n\n private check(): void {\n this.lastCheck = Date.now();\n this.swUpdate.checkForUpdate().catch(() => {\n // Offline, or the worker is gone. The next scheduled check tries again.\n });\n }\n\n private reload(): void {\n if (this.leaving) return;\n this.leaving = true;\n this.pageLoader.reload();\n }\n\n private isHidden(): boolean {\n return this.document.visibilityState === 'hidden';\n }\n\n private readReloadedFor(): string | null {\n try {\n return this.document.defaultView?.sessionStorage.getItem(RELOADED_FOR_KEY) ?? null;\n } catch {\n return null;\n }\n }\n\n private writeReloadedFor(hash: string | undefined): void {\n if (!hash) return;\n try {\n this.document.defaultView?.sessionStorage.setItem(RELOADED_FOR_KEY, hash);\n } catch {\n // Storage blocked: the guard is best effort; one reload per navigation is still bounded.\n }\n }\n}\n\nfunction toVersion(version: { hash: string; appData?: object }): SparkAppVersion {\n return { hash: version.hash, appData: version.appData as Record<string, unknown> | undefined };\n}\n","import { EnvironmentProviders, inject, isDevMode, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';\nimport { SwRegistrationOptions, provideServiceWorker } from '@angular/service-worker';\n\nimport { SparkNotificationClicks } from './spark-notification-clicks';\nimport { SparkServiceWorkerUpdates } from './spark-service-worker-updates';\n\nexport interface SparkServiceWorkerOptions {\n /** Whether the worker registers. Default: `!isDevMode()`, so only production builds register it. */\n enabled?: boolean;\n /** The worker script, relative to the base href. Default: `ngsw-worker.js`. */\n script?: string;\n /** When the worker registers. Default: `registerWhenStable:30000`. */\n registrationStrategy?: SwRegistrationOptions['registrationStrategy'];\n}\n\n/**\n * Registers Angular's service worker with Spark's update policy (#464, Q1b); see\n * {@link SparkServiceWorkerUpdates} for what happens when a new version is deployed, and\n * {@link SparkNotificationClicks} for a push notification clicked while the application is open.\n *\n * The application also needs an `ngsw-config.json` with Spark's navigation exclusions and\n * `serviceWorker` in its production build configuration; see `docs/guide-pwa.md`.\n */\nexport function provideSparkServiceWorker(options: SparkServiceWorkerOptions = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n provideServiceWorker(options.script ?? 'ngsw-worker.js', {\n enabled: options.enabled ?? !isDevMode(),\n registrationStrategy: options.registrationStrategy ?? 'registerWhenStable:30000',\n }),\n provideAppInitializer(() => {\n inject(SparkServiceWorkerUpdates).start();\n inject(SparkNotificationClicks).start();\n }),\n ]);\n}\n"],"mappings":";;;;;;AAWA,SAAgB,cAAc,KAAgC,UAAoB,UAAyB;CACzG,IAAI,CAAC,KAAK,OAAO;CACjB,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,KAAK,SAAS,OAAO;CACxC,QAAQ;EACN,OAAO;CACT;CACA,IAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU,OAAO;CACxE,IAAI,OAAO,WAAW,SAAS,UAAU,QAAQ,OAAO;CACxD,MAAM,OAAO,WAAW,SAAS,UAAU,OAAO,QAAQ,IAAI,OAAO;CACrE,QAAQ,KAAK,WAAW,GAAG,IAAI,OAAO,MAAM,QAAQ,OAAO,SAAS,OAAO;AAC7E;ACEA,IAAa,0BAAb,MAAa,wBAAuB;CACjB,SAAS,OAAO,MAAM;CACtB,SAAS,OAAO,QAAQ,EAAE,UAAU,KAAI,CAAE;CAC1C,WAAW,OAAO,UAAU,EAAE,UAAU,KAAI,CAAE;CAC9C,WAAW,OAAO,QAAQ;CAC1B,aAAa,OAAO,WAAW;CAC/B,aAAa,OAAO,UAAU;CAEvC,UAAU;CAGlB,QAAK;EACH,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU;EACf,IAAI,CAAC,kBAAkB,KAAK,UAAU,KAAK,CAAC,KAAK,OAAO,aAAa,CAAC,KAAK,QAAQ;EAEnF,MAAM,eAAe,KAAK,OAAO,mBAAmB,WAAW,EAAE,mBAAkB;GACjF,MAAM,OAAO,cAAc;GAC3B,KAAK,SAAS,OAAO,MAAM,QAAQ,WAAW,KAAK,MAAM,IAAI;EAC/D,CAAC;EACD,KAAK,WAAW,gBAAgB,aAAa,YAAW,CAAE;CAC5D;CAEQ,SAAS,KAAkB;EACjC,MAAM,SAAS,cAAc,KAAK,KAAK,UAAU,KAAK,QAAQ;EAE9D,IAAI,CAAC,UAAU,CAAC,KAAK,UAAU,KAAK,OAAO,QAAQ,QAAQ;EAE3D,KAAK,OAAO,cAAc,MAAM,CAAC,CAAC,YAAY,KAAK,OAAO,IAAI,IAAI,KAAM,KAAK,SAAS,OAAO,CAAC,CAAC,IAAI,CAAC;CACtG;CAGU,OAAO,MAAY;EAC3B,KAAK,SAAS,aAAa,SAAS,OAAO,IAAI;CACjD;CAlCW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAuB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAvB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAuB,YADV;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAuB,YAAA,CAAA;EADnC,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;ACGlC,MAAa,uBAAuB,IAAI,eAAgC,wBAAwB;CAC9F,YAAY;CACZ,eAAc;EACZ,MAAM,WAAW,OAAO,QAAQ,CAAC,CAAC,aAAa;EAC/C,OAAO;GACL,SAAS,QAAgB,UAAU,OAAO,GAAG;GAC7C,cAAc,UAAU,OAAM;EAC/B;CACH;AACD,CAAA;AAQD,MAAa,oBAAoB,IAAI,eAA2C,qBAAqB;CACnG,YAAY;CACZ,gBAAgB;EAAE,iBAAiB;EAAoB,wBAAwB;CAAS;AACzF,CAAA;AAOD,MAAM,mBAAmB;AAmBzB,IAAa,4BAAb,MAAa,0BAAyB;CACnB,WAAW,OAAO,QAAQ;CAC1B,SAAS,OAAO,QAAQ,EAAE,UAAU,KAAI,CAAE;CAC1C,WAAW,OAAO,UAAU,EAAE,UAAU,KAAI,CAAE;CAC9C,SAAS,OAAO,cAAc;CAC9B,WAAW,OAAO,QAAQ;CAC1B,aAAa,OAAO,WAAW;CAC/B,aAAa,OAAO,UAAU;CAC9B,aAAa,OAAO,oBAAoB;CACxC,WAAW,OAAO,iBAAiB;CAEnC,eAAe,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAC5B,kBAAkB,OAA+B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CACtD,iBAAiB,OAA+B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAG7D,cAAc,KAAK,aAAa,WAAU;CAE1C,iBAAiB,KAAK,gBAAgB,WAAU;CAEhD,gBAAgB,KAAK,eAAe,WAAU;CAE/C,UAAU;CACV,UAAU;CACV,oBAAoB;CACpB,YAAY;CAGpB,QAAK;EACH,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU;EAEf,IAAI,CAAC,kBAAkB,KAAK,UAAU,KAAK,CAAC,KAAK,SAAS,WAAW;EAErE,MAAM,gBAAgB,IAAI,aAAY;EACtC,KAAK,WAAW,gBAAgB,cAAc,YAAW,CAAE;EAE3D,cAAc,IACZ,KAAK,SAAS,eACX,KAAK,QAAQ,UAAsC,MAAM,SAAS,eAAe,CAAC,CAAC,CACnF,WAAW,UAAU,KAAK,eAAe,KAAK,CAAC,CAAC;EAGrD,cAAc,IAAI,KAAK,SAAS,cAAc,gBAAgB,KAAK,gBAAe,CAAE,CAAC;EAErF,IAAI,KAAK,QACP,cAAc,IACZ,KAAK,OAAO,OACT,KAAK,QAAQ,UAAoC,iBAAiB,eAAe,CAAC,CAAC,CACnF,WAAW,UAAU,KAAK,kBAAkB,KAAK,CAAC,CAAC;EAK1D,cAAc,IACZ,KAAK,OAAO,SAAS,KAAK,OAAO,WAAW,MAAM,CAAC,CAAC,CAAC,gBAAe;GAClE,KAAK,MAAK;GACV,MAAM,WAAW,kBAAkB,KAAK,MAAK,GAAI,KAAK,SAAS,eAAe;GAC9E,cAAc,UAAU,cAAc,QAAQ,CAAC;EACjD,CAAC,CAAC;EAGJ,MAAM,2BAA0B;GAC9B,IAAI,KAAK,SAAQ,GAAI;GACrB,IAAI,KAAK,mBAAmB;IAC1B,KAAK,oBAAoB;IACzB,KAAK,OAAM;IACX;GACF;GACA,IAAI,KAAK,IAAG,IAAK,KAAK,aAAa,KAAK,SAAS,wBAAwB,KAAK,MAAK;EACrF;EACA,KAAK,SAAS,iBAAiB,oBAAoB,kBAAkB;EACrE,cAAc,UAAU,KAAK,SAAS,oBAAoB,oBAAoB,kBAAkB,CAAC;CACnG;CAEQ,eAAe,OAAwB;EAC7C,MAAM,SAAS,UAAU,MAAM,aAAa;EAC5C,KAAK,gBAAgB,IAAI,UAAU,MAAM,cAAc,CAAC;EACxD,KAAK,eAAe,IAAI,MAAM;EAE9B,IAAI,KAAK,gBAAe,MAAO,OAAO,MAGpC;EAEF,KAAK,aAAa,IAAI,IAAI;CAC5B;CAEQ,kBAAe;EACrB,IAAI,KAAK,SAAQ,GAAI;GACnB,KAAK,oBAAoB;GACzB;EACF;EACA,KAAK,OAAM;CACb;CAEQ,kBAAkB,OAAsB;EAC9C,IAAI,CAAC,KAAK,aAAY,KAAM,KAAK,WAAW,KAAK,SAAQ,GAAI;EAE7D,KAAK,UAAU;EACf,KAAK,iBAAiB,KAAK,eAAc,CAAE,EAAE,IAAI;EACjD,KAAK,QAAQ,kBAAiB,CAAE,EAAE,MAAK;EAEvC,IAAI,MAAM,sBAAsB,YAAY;GAG1C,KAAK,WAAW,OAAM;GACtB;EACF;EAGA,MAAM,SAAS,KAAK,WAAW,KAAK,SAAS,mBAAmB,MAAM,GAAG,IAAI,MAAM;EACnF,KAAK,WAAW,OAAO,MAAM;CAC/B;CAEQ,QAAK;EACX,KAAK,YAAY,KAAK,IAAG;EACzB,KAAK,SAAS,eAAc,CAAE,CAAC,YAAW,CAE1C,CAAC;CACH;CAEQ,SAAM;EACZ,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU;EACf,KAAK,WAAW,OAAM;CACxB;CAEQ,WAAQ;EACd,OAAO,KAAK,SAAS,oBAAoB;CAC3C;CAEQ,kBAAe;EACrB,IAAI;GACF,OAAO,KAAK,SAAS,aAAa,eAAe,QAAQ,gBAAgB,KAAK;EAChF,QAAQ;GACN,OAAO;EACT;CACF;CAEQ,iBAAiB,MAAwB;EAC/C,IAAI,CAAC,MAAM;EACX,IAAI;GACF,KAAK,SAAS,aAAa,eAAe,QAAQ,kBAAkB,IAAI;EAC1E,QAAQ,CAER;CACF;CAnJW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAyB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAzB,OAAA,QAAA,GAAA,sBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAyB,YADZ;CAAM,CAAA;;AACnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAyB,YAAA,CAAA;EADrC,MAAA;EAAW,MAAA,CAAA,EAAE,YAAY,OAAM,CAAE;;;AAuJlC,SAAS,UAAU,SAA2C;CAC5D,OAAO;EAAE,MAAM,QAAQ;EAAM,SAAS,QAAQ;CAA8C;AAC9F;AC1MA,SAAgB,0BAA0B,UAAqC,CAAA,GAAE;CAC/E,OAAO,yBAAyB,CAC9B,qBAAqB,QAAQ,UAAU,kBAAkB;EACvD,SAAS,QAAQ,WAAW,CAAC,UAAS;EACtC,sBAAsB,QAAQ,wBAAwB;CACvD,CAAA,GACD,4BAA2B;EACzB,OAAO,yBAAyB,CAAC,CAAC,MAAK;EACvC,OAAO,uBAAuB,CAAC,CAAC,MAAK;CACvC,CAAC,CACF,CAAA;AACH"}
@@ -1,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, viewChild } from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, computed, inject, input, output, signal, viewChild } from "@angular/core";
3
3
  import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
4
4
  import * as i1 from "@angular/common";
5
5
  import { CommonModule, NgComponentOutlet, NgTemplateOutlet } from "@angular/common";
@@ -10,18 +10,15 @@ import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
10
10
  import { BsGridColumnDirective, BsGridComponent, BsGridRowDirective } from "@mintplayer/ng-bootstrap/grid";
11
11
  import { BsPriorityNavComponent, BsPriorityNavItemDirective } from "@mintplayer/ng-bootstrap/priority-nav";
12
12
  import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
13
- import { SparkLanguageService, SparkService, SparkStreamingService } from "@mintplayer/ng-spark/services";
13
+ import { SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
14
14
  import { ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
15
15
  import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
16
16
  import { SparkQueryGridComponent, SparkSearchBoxComponent, sparkActionClass } from "@mintplayer/ng-spark/grid";
17
- import { isDateDataType, parseWireDate } from "@mintplayer/ng-spark/models";
18
17
  import { SPARK_QUERY_LIST_ACTIONS, orderSparkExtensions, parseSparkDeletedParam } from "@mintplayer/ng-spark/panels";
19
18
  var SparkQueryListComponent = class SparkQueryListComponent {
20
19
  route = inject(ActivatedRoute);
21
20
  router = inject(Router);
22
21
  sparkService = inject(SparkService);
23
- streamingService = inject(SparkStreamingService);
24
- destroyRef = inject(DestroyRef);
25
22
  lang = inject(SparkLanguageService);
26
23
  extraActionsTemplate = input(null, ...ngDevMode ? [{ debugName: "extraActionsTemplate" }] : /* istanbul ignore next */ []);
27
24
  showCustomActions = input(true, ...ngDevMode ? [{ debugName: "showCustomActions" }] : /* istanbul ignore next */ []);
@@ -42,6 +39,7 @@ var SparkQueryListComponent = class SparkQueryListComponent {
42
39
  resultCount = computed(() => this.grid()?.resultCount() ?? null, ...ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []);
43
40
  isVirtualScrolling = computed(() => this.grid()?.isVirtualScrolling() ?? false, ...ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []);
44
41
  gridError = computed(() => this.grid()?.errorMessage() ?? null, ...ngDevMode ? [{ debugName: "gridError" }] : /* istanbul ignore next */ []);
42
+ isLive = computed(() => this.grid()?.isLive() ?? false, ...ngDevMode ? [{ debugName: "isLive" }] : /* istanbul ignore next */ []);
45
43
  permissions = computed(() => this.grid()?.permissions() ?? null, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
46
44
  listActions = orderSparkExtensions(inject(SPARK_QUERY_LIST_ACTIONS, { optional: true }), (a) => a.priority ?? 60);
47
45
  deletedMode = signal("exclude", ...ngDevMode ? [{ debugName: "deletedMode" }] : /* istanbul ignore next */ []);
@@ -98,12 +96,6 @@ var SparkQueryListComponent = class SparkQueryListComponent {
98
96
  isActionEnabled(action) {
99
97
  return this.grid()?.isActionEnabled(action) ?? false;
100
98
  }
101
- isStreaming = signal(false, ...ngDevMode ? [{ debugName: "isStreaming" }] : /* istanbul ignore next */ []);
102
- streamingSub = null;
103
- streamColumns = signal([], ...ngDevMode ? [{ debugName: "streamColumns" }] : /* istanbul ignore next */ []);
104
- allItems = signal([], ...ngDevMode ? [{ debugName: "allItems" }] : /* istanbul ignore next */ []);
105
- streamItems = signal([], ...ngDevMode ? [{ debugName: "streamItems" }] : /* istanbul ignore next */ []);
106
- gridData = computed(() => this.query()?.isStreamingQuery ? this.streamItems() : null, ...ngDevMode ? [{ debugName: "gridData" }] : /* istanbul ignore next */ []);
107
99
  constructor() {
108
100
  this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => {
109
101
  this.onParamsChange(params).catch((e) => this.reportLoadFailure(e));
@@ -112,18 +104,6 @@ var SparkQueryListComponent = class SparkQueryListComponent {
112
104
  this.deletedMode.set(parseSparkDeletedParam(query.get("deleted")) ?? "exclude");
113
105
  this.applyUrlScope(query);
114
106
  });
115
- this.destroyRef.onDestroy(() => this.disconnectStreaming());
116
- effect(() => {
117
- const q = this.query();
118
- if (q?.isStreamingQuery) this.connectStreaming(q.id);
119
- else this.disconnectStreaming();
120
- });
121
- effect(() => {
122
- this.searchTerm();
123
- this.grid()?.settings();
124
- this.allItems();
125
- if (this.isStreaming()) this.applyFilter();
126
- });
127
107
  }
128
108
  urlParentId = signal("", ...ngDevMode ? [{ debugName: "urlParentId" }] : /* istanbul ignore next */ []);
129
109
  urlParentType = signal("", ...ngDevMode ? [{ debugName: "urlParentType" }] : /* istanbul ignore next */ []);
@@ -179,10 +159,6 @@ var SparkQueryListComponent = class SparkQueryListComponent {
179
159
  async onParamsChange(params) {
180
160
  this.errorMessage.set(null);
181
161
  this.queryId.set(null);
182
- this.allItems.set([]);
183
- this.streamItems.set([]);
184
- this.streamColumns.set([]);
185
- this.disconnectStreaming();
186
162
  const queryId = params.get("queryId");
187
163
  const typeParam = params.get("type");
188
164
  if (queryId) {
@@ -206,7 +182,7 @@ var SparkQueryListComponent = class SparkQueryListComponent {
206
182
  else this.reportLoadFailure({ status: 404 });
207
183
  }
208
184
  reportLoadFailure(err) {
209
- this.errorMessage.set(err?.status === 404 ? this.lang.t("spark.query.unavailable") || "This list is not available." : err?.error?.error || err?.message || "An unexpected error occurred");
185
+ this.errorMessage.set(err?.status === 404 ? this.lang.t("spark.query.unavailable", "This list is not available.") : err?.error?.error || err?.message || "An unexpected error occurred");
210
186
  }
211
187
  async onCustomAction(action) {
212
188
  await this.grid()?.onCustomAction(action);
@@ -223,70 +199,9 @@ var SparkQueryListComponent = class SparkQueryListComponent {
223
199
  clearSearch() {
224
200
  this.searchTerm.set("");
225
201
  }
226
- connectStreaming(queryId) {
227
- if (this.streamingSub) return;
228
- this.isStreaming.set(true);
229
- this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({
230
- next: (message) => this.handleStreamingMessage(message),
231
- error: (err) => {
232
- this.errorMessage.set(err?.message || "Streaming connection failed");
233
- this.isStreaming.set(false);
234
- },
235
- complete: () => this.isStreaming.set(false)
236
- });
237
- }
238
- disconnectStreaming() {
239
- if (this.streamingSub) {
240
- this.streamingSub.unsubscribe();
241
- this.streamingSub = null;
242
- }
243
- this.isStreaming.set(false);
244
- }
245
- handleStreamingMessage(message) {
246
- switch (message.type) {
247
- case "snapshot":
248
- this.errorMessage.set(null);
249
- this.streamColumns.set(message.columns);
250
- this.allItems.set(message.data);
251
- break;
252
- case "patch":
253
- if (message.updated.length > 0) this.allItems.update((items) => items.map((item) => {
254
- const patch = message.updated.find((u) => u.id === item.id);
255
- if (!patch) return item;
256
- return {
257
- ...item,
258
- values: item.values.map((v) => v.key in patch.values ? {
259
- ...v,
260
- value: patch.values[v.key]
261
- } : v)
262
- };
263
- }));
264
- break;
265
- case "error": this.errorMessage.set(message.message);
266
- }
267
- }
268
- applyFilter() {
269
- let items = this.allItems();
270
- const term = this.searchTerm().toLowerCase();
271
- if (term) items = items.filter((item) => item.values.some((v) => String(v.value ?? "").toLowerCase().includes(term)));
272
- const sortCols = this.grid()?.settings().sortColumns ?? [];
273
- if (sortCols.length > 0) {
274
- const dataTypes = new Map(this.streamColumns().map((c) => [c.name, c.dataType]));
275
- items = [...items].sort((a, b) => {
276
- for (const col of sortCols) {
277
- const aVal = a.values.find((v) => v.key === col.property)?.value;
278
- const bVal = b.values.find((v) => v.key === col.property)?.value;
279
- const cmp = compareStreamValues(aVal, bVal, dataTypes.get(col.property));
280
- if (cmp !== 0) return col.direction === "descending" ? -cmp : cmp;
281
- }
282
- return 0;
283
- });
284
- }
285
- this.streamItems.set(items);
286
- }
287
202
  static ɵfac = i0.ɵɵngDeclareFactory({
288
203
  minVersion: "12.0.0",
289
- version: "22.2.0",
204
+ version: "22.2.2",
290
205
  ngImport: i0,
291
206
  type: SparkQueryListComponent,
292
207
  deps: [],
@@ -294,7 +209,7 @@ var SparkQueryListComponent = class SparkQueryListComponent {
294
209
  });
295
210
  static ɵcmp = i0.ɵɵngDeclareComponent({
296
211
  minVersion: "17.0.0",
297
- version: "22.2.0",
212
+ version: "22.2.2",
298
213
  type: SparkQueryListComponent,
299
214
  isStandalone: true,
300
215
  selector: "spark-query-list",
@@ -342,7 +257,7 @@ var SparkQueryListComponent = class SparkQueryListComponent {
342
257
  isSignal: true
343
258
  }],
344
259
  ngImport: i0,
345
- template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460 M15, #467 R3): New, Edit, Delete and the custom actions —\n the list the sub-query card renders too, labels and icons from the action catalogue.\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" [attr.title]=\"toolbarActionUnavailableReason(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.label | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.label | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
260
+ template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460 M15, #467 R3): New, Edit, Delete and the custom actions —\n the list the sub-query card renders too, labels and icons from the action catalogue.\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" [attr.title]=\"toolbarActionUnavailableReason(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.label | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.label | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isLive()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid. It fetches and pages server-side, over HTTP or, for a live query, over /spark/live\n (the grid owns that, so embedded grids follow live queries too). This used to be two\n near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
346
261
  styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--spark-shell-main-bg, var(--bs-body-bg));border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 calc(-1 * var(--spark-main-padding, 1.5rem)) calc(-1 * var(--spark-main-padding, 1.5rem));--spark-content-inset-inline: 0px}:host.virtual-scrolling spark-query-grid{display:flex;flex-direction:column;min-height:0}:host.virtual-scrolling ::ng-deep bs-datatable{display:flex;flex-direction:column;min-height:0;--mp-datatable-virtual-max-height: 100%}:host.virtual-scrolling ::ng-deep bs-datatable mp-datatable{flex:1 1 auto;min-height:0}tr:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.05)}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}.spark-live-badge{font-size:.6em}.spark-url-filter-clear{background:none;border:0;padding:0 0 0 .25rem;color:inherit;line-height:1}\n"],
347
262
  dependencies: [
348
263
  {
@@ -461,6 +376,7 @@ var SparkQueryListComponent = class SparkQueryListComponent {
461
376
  "parentType",
462
377
  "presetFilters",
463
378
  "data",
379
+ "live",
464
380
  "search",
465
381
  "columns",
466
382
  "reloadToken",
@@ -507,7 +423,7 @@ var SparkQueryListComponent = class SparkQueryListComponent {
507
423
  };
508
424
  i0.ɵɵngDeclareClassMetadata({
509
425
  minVersion: "12.0.0",
510
- version: "22.2.0",
426
+ version: "22.2.2",
511
427
  ngImport: i0,
512
428
  type: SparkQueryListComponent,
513
429
  decorators: [{
@@ -534,7 +450,7 @@ i0.ɵɵngDeclareClassMetadata({
534
450
  ],
535
451
  changeDetection: ChangeDetectionStrategy.OnPush,
536
452
  host: { "[class.virtual-scrolling]": "isVirtualScrolling()" },
537
- template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460 M15, #467 R3): New, Edit, Delete and the custom actions —\n the list the sub-query card renders too, labels and icons from the action catalogue.\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" [attr.title]=\"toolbarActionUnavailableReason(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.label | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.label | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
453
+ template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460 M15, #467 R3): New, Edit, Delete and the custom actions —\n the list the sub-query card renders too, labels and icons from the action catalogue.\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" [attr.title]=\"toolbarActionUnavailableReason(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.label | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.label | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isLive()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid. It fetches and pages server-side, over HTTP or, for a live query, over /spark/live\n (the grid owns that, so embedded grids follow live queries too). This used to be two\n near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
538
454
  styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--spark-shell-main-bg, var(--bs-body-bg));border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 calc(-1 * var(--spark-main-padding, 1.5rem)) calc(-1 * var(--spark-main-padding, 1.5rem));--spark-content-inset-inline: 0px}:host.virtual-scrolling spark-query-grid{display:flex;flex-direction:column;min-height:0}:host.virtual-scrolling ::ng-deep bs-datatable{display:flex;flex-direction:column;min-height:0;--mp-datatable-virtual-max-height: 100%}:host.virtual-scrolling ::ng-deep bs-datatable mp-datatable{flex:1 1 auto;min-height:0}tr:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.05)}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}.spark-live-badge{font-size:.6em}.spark-url-filter-clear{background:none;border:0;padding:0 0 0 .25rem;color:inherit;line-height:1}\n"]
539
455
  }]
540
456
  }],
@@ -590,21 +506,6 @@ i0.ɵɵngDeclareClassMetadata({
590
506
  }]
591
507
  }
592
508
  });
593
- function compareStreamValues(a, b, dataType) {
594
- const aMissing = a === null || a === void 0 || a === "";
595
- const bMissing = b === null || b === void 0 || b === "";
596
- if (aMissing || bMissing) return aMissing === bMissing ? 0 : aMissing ? -1 : 1;
597
- if (isDateDataType(dataType)) {
598
- const aDate = parseWireDate(a);
599
- const bDate = parseWireDate(b);
600
- if (aDate && bDate) return aDate.getTime() - bDate.getTime();
601
- } else if (dataType === "number" || dataType === "decimal") {
602
- const aNumber = Number(a);
603
- const bNumber = Number(b);
604
- if (!isNaN(aNumber) && !isNaN(bNumber)) return aNumber - bNumber;
605
- }
606
- return String(a).localeCompare(String(b));
607
- }
608
509
  export { SparkQueryListComponent };
609
510
 
610
511
  //# sourceMappingURL=mintplayer-ng-spark-query-list.mjs.map