@mintplayer/ng-spark 22.32.0 → 22.34.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 (81) 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-dashboards.mjs +726 -0
  20. package/fesm2022/mintplayer-ng-spark-dashboards.mjs.map +1 -0
  21. package/fesm2022/mintplayer-ng-spark-grid.mjs +220 -49
  22. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  23. package/fesm2022/mintplayer-ng-spark-history.mjs +9 -9
  24. package/fesm2022/mintplayer-ng-spark-icon.mjs +3 -3
  25. package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs +3 -3
  26. package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs +9 -9
  27. package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs +6 -6
  28. package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs +3 -3
  29. package/fesm2022/mintplayer-ng-spark-models.mjs +44 -1
  30. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  31. package/fesm2022/mintplayer-ng-spark-moderation.mjs +21 -21
  32. package/fesm2022/mintplayer-ng-spark-notifications.mjs +687 -0
  33. package/fesm2022/mintplayer-ng-spark-notifications.mjs.map +1 -0
  34. package/fesm2022/mintplayer-ng-spark-organizations-accept.mjs +179 -0
  35. package/fesm2022/mintplayer-ng-spark-organizations-accept.mjs.map +1 -0
  36. package/fesm2022/mintplayer-ng-spark-organizations.mjs +202 -0
  37. package/fesm2022/mintplayer-ng-spark-organizations.mjs.map +1 -0
  38. package/fesm2022/mintplayer-ng-spark-panels.mjs +9 -1
  39. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-pipes.mjs +125 -85
  41. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-po-create.mjs +3 -3
  43. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +95 -29
  44. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  45. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +84 -12
  46. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  47. package/fesm2022/mintplayer-ng-spark-po-form.mjs +9 -9
  48. package/fesm2022/mintplayer-ng-spark-pwa.mjs +74 -6
  49. package/fesm2022/mintplayer-ng-spark-pwa.mjs.map +1 -1
  50. package/fesm2022/mintplayer-ng-spark-query-list.mjs +10 -109
  51. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  52. package/fesm2022/mintplayer-ng-spark-renderers.mjs +3 -3
  53. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +3 -3
  54. package/fesm2022/mintplayer-ng-spark-score-renderers.mjs +801 -0
  55. package/fesm2022/mintplayer-ng-spark-score-renderers.mjs.map +1 -0
  56. package/fesm2022/mintplayer-ng-spark-services.mjs +424 -72
  57. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  58. package/fesm2022/mintplayer-ng-spark-share-page.mjs +334 -0
  59. package/fesm2022/mintplayer-ng-spark-share-page.mjs.map +1 -0
  60. package/fesm2022/mintplayer-ng-spark-share.mjs +10 -0
  61. package/fesm2022/mintplayer-ng-spark-share.mjs.map +1 -0
  62. package/fesm2022/mintplayer-ng-spark-shell.mjs +38 -36
  63. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  64. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +9 -9
  65. package/package.json +29 -1
  66. package/types/mintplayer-ng-spark-dashboards.d.ts +226 -0
  67. package/types/mintplayer-ng-spark-grid.d.ts +77 -26
  68. package/types/mintplayer-ng-spark-models.d.ts +150 -17
  69. package/types/mintplayer-ng-spark-notifications.d.ts +188 -0
  70. package/types/mintplayer-ng-spark-organizations-accept.d.ts +40 -0
  71. package/types/mintplayer-ng-spark-organizations.d.ts +72 -0
  72. package/types/mintplayer-ng-spark-panels.d.ts +16 -0
  73. package/types/mintplayer-ng-spark-pipes.d.ts +10 -0
  74. package/types/mintplayer-ng-spark-po-detail.d.ts +29 -0
  75. package/types/mintplayer-ng-spark-po-edit.d.ts +37 -0
  76. package/types/mintplayer-ng-spark-pwa.d.ts +48 -2
  77. package/types/mintplayer-ng-spark-query-list.d.ts +8 -23
  78. package/types/mintplayer-ng-spark-score-renderers.d.ts +340 -0
  79. package/types/mintplayer-ng-spark-services.d.ts +146 -6
  80. package/types/mintplayer-ng-spark-share-page.d.ts +67 -0
  81. package/types/mintplayer-ng-spark-share.d.ts +26 -0
@@ -0,0 +1,188 @@
1
+ import * as i0 from "@angular/core";
2
+ import { InjectionToken, Provider, Signal } from "@angular/core";
3
+ import { QueryResultItem } from "@mintplayer/ng-spark/models";
4
+ import { ConnectedPosition } from "@angular/cdk/overlay";
5
+ import { Color } from "@mintplayer/ng-bootstrap";
6
+ /** The `Notification` entity type of `MintPlayer.Spark.Notifications` (`App_Data/Model/Notification.json`). */
7
+ export declare const SPARK_NOTIFICATION_TYPE_ID = "411e3be6-5da0-5d84-bc21-001888fafc6d";
8
+ /** `MyNotifications` (alias `notifications`): the caller's notifications, newest first. Live. */
9
+ export declare const SPARK_MY_NOTIFICATIONS_QUERY_ID = "7123c201-9aed-50f9-9d9a-ede2491af7f9";
10
+ /** `UnreadNotifications` (alias `unread-notifications`): the caller's unread ones. Live. */
11
+ export declare const SPARK_UNREAD_NOTIFICATIONS_QUERY_ID = "a205d3e5-ff6e-5260-ac9e-e5c6b16644c9";
12
+ /** One notification as the bell lists it: a row of `MyNotifications`. */
13
+ export interface SparkNotification {
14
+ id: string;
15
+ title: string;
16
+ body: string | null;
17
+ /**
18
+ * The link, when the row carries it. `Url` is a detail-only attribute in the shipped model, so a row
19
+ * usually does not, and {@link SparkNotificationsService.open} loads it from the object instead.
20
+ */
21
+ url: string | null;
22
+ isRead: boolean;
23
+ createdAt: Date | null;
24
+ }
25
+ /** A query row of `MyNotifications` as a {@link SparkNotification}. */
26
+ export declare function toSparkNotification(item: QueryResultItem): SparkNotification;
27
+ /** Options of the notification bell and service. */
28
+ export interface SparkNotificationsOptions {
29
+ /** How many of the latest notifications the bell lists. Default 8. */
30
+ recentCount: number;
31
+ /** Where "All notifications" goes: a router link. Default `['/query', 'notifications']` (the `MyNotifications` alias). */
32
+ listLink: string | readonly unknown[];
33
+ }
34
+ export declare const SPARK_NOTIFICATIONS_OPTIONS: InjectionToken<SparkNotificationsOptions>;
35
+ /** Overrides {@link SparkNotificationsOptions}; anything left out keeps its default. */
36
+ export declare function provideSparkNotifications(options?: Partial<SparkNotificationsOptions>): Provider;
37
+ /** The browser's Notification API, as far as push needs it. */
38
+ export interface SparkNotificationPermissions {
39
+ readonly permission: NotificationPermission;
40
+ requestPermission(): Promise<NotificationPermission>;
41
+ }
42
+ /**
43
+ * What the browser offers for Web Push: the Notification API, or `null` when there is no Notification,
44
+ * no `PushManager` or no service worker. That is the case in Safari on iPhone and iPad until the app is
45
+ * added to the Home Screen, so `null` reads as "install the app first". Replaceable for tests.
46
+ */
47
+ export declare const SPARK_PUSH_PERMISSIONS: InjectionToken<SparkNotificationPermissions | null>;
48
+ /**
49
+ * Web Push on this device: `unsupported` (no service worker, no PushManager or no Notification API:
50
+ * an iPhone/iPad needs the app on its Home Screen), `denied` (blocked in the browser's settings),
51
+ * `off` or `on`.
52
+ */
53
+ export type SparkPushState = 'unsupported' | 'denied' | 'off' | 'on';
54
+ /**
55
+ * The client of `MintPlayer.Spark.Notifications` (`docs/seo_analyzer_PRD.md` X10, D20, D30).
56
+ *
57
+ * - **In-app**: {@link unreadCount} and {@link recent} follow the live queries `UnreadNotifications`
58
+ * (one row, for its `totalItems`) and `MyNotifications` (the latest few) over the ONE `/spark/live`
59
+ * socket of {@link SparkLiveService}. Nothing polls and there is no second channel: a new, read or
60
+ * deleted notification arrives as a patch. The subscriptions start again whenever `SPARK_AUTH_STATE`
61
+ * changes (sign-in, sign-out), so a page opened before signing in still counts afterwards.
62
+ * - **Web Push**: {@link enablePush} / {@link disablePush} subscribe this device through Angular's
63
+ * `SwPush` and register it at `/spark/notifications/push/*`. It needs `provideSparkServiceWorker()`
64
+ * (or `provideServiceWorker`) with the worker enabled; without it {@link pushState} is `unsupported`.
65
+ */
66
+ export declare class SparkNotificationsService {
67
+ private readonly http;
68
+ private readonly live;
69
+ private readonly spark;
70
+ private readonly router;
71
+ private readonly location;
72
+ private readonly document;
73
+ private readonly swPush;
74
+ private readonly permissions;
75
+ private readonly options;
76
+ private readonly baseUrl;
77
+ private readonly browser;
78
+ private readonly unread;
79
+ private readonly latest;
80
+ /** Unread notifications of the signed-in user; 0 until the first result and while signed out. */
81
+ readonly unreadCount: Signal<number>;
82
+ /** The latest notifications (read or not), newest first; empty until the first result. */
83
+ readonly recent: Signal<SparkNotification[]>;
84
+ /** Whether {@link recent} holds a result yet. */
85
+ readonly loaded: Signal<boolean>;
86
+ /** Whether this browser can receive Web Push here at all (see {@link SparkPushState}). */
87
+ readonly pushSupported: boolean;
88
+ private readonly permission;
89
+ private readonly subscribed;
90
+ private readonly configured;
91
+ private readonly busy;
92
+ private publicKey;
93
+ /** Web Push on this device. */
94
+ readonly pushState: Signal<SparkPushState>;
95
+ /**
96
+ * Whether the server sends Web Push (`GET /spark/notifications/push/key` answered a key): `null` while
97
+ * unknown, `false` when Web Push is not configured or nobody is signed in. Offer no push UI unless `true`.
98
+ */
99
+ readonly pushConfigured: Signal<boolean | null>;
100
+ /** True while {@link enablePush} or {@link disablePush} runs. */
101
+ readonly pushBusy: Signal<boolean>;
102
+ constructor();
103
+ /**
104
+ * Marks notifications read: the given ids, or every unread one of the caller when `ids` is omitted.
105
+ * Resolves to how many changed. The live queries then deliver the new state; nothing is set locally.
106
+ */
107
+ markRead(ids?: readonly string[]): Promise<number>;
108
+ /**
109
+ * Opens a notification: marks it read when it is not, then follows its link, in-app through the router
110
+ * for a URL on this origin, as a page load for another `http(s)` URL. A row without its `Url` (a
111
+ * detail-only attribute in the shipped model) is loaded once for it.
112
+ */
113
+ open(notification: SparkNotification): Promise<void>;
114
+ /**
115
+ * Turns Web Push on for this device. Call it straight from a click handler: the permission prompt is
116
+ * requested before anything is awaited, because Safari only grants it to a user gesture. Resolves to
117
+ * whether the device is now subscribed; a refusal, a missing key or a subscription the server rejects
118
+ * (400: a push service outside its allow-list) is `false`.
119
+ */
120
+ enablePush(): Promise<boolean>;
121
+ /** Turns Web Push off for this device: the server forgets it, then the browser unsubscribes. */
122
+ disablePush(): Promise<void>;
123
+ private follow;
124
+ private loadPublicKey;
125
+ private loadUrl;
126
+ private navigate;
127
+ /** A full page load (overridden in specs: jsdom cannot navigate). */
128
+ protected assign(href: string): void;
129
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkNotificationsService, never>;
130
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
131
+ }
132
+ /** English fallbacks of `notifications.spa.*`, shown until the server's translations arrive. */
133
+ export declare const SPARK_NOTIFICATION_TEXTS: Record<string, string>;
134
+ /**
135
+ * The Web Push opt-in for this device, on its own (an account or settings page); the notification bell
136
+ * shows the same control. Renders nothing unless the server sends Web Push. Otherwise: a button to turn
137
+ * it on or off, or why it cannot be (blocked in the browser, or a browser without push: on iPhone and
138
+ * iPad the app must be on the Home Screen first).
139
+ */
140
+ export declare class SparkPushToggleComponent {
141
+ protected readonly notifications: SparkNotificationsService;
142
+ private readonly language;
143
+ protected t(key: string): string;
144
+ /** Synchronous up to the permission prompt: this is the click handler the prompt needs. */
145
+ protected enable(): void;
146
+ protected disable(): void;
147
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkPushToggleComponent, never>;
148
+ static ɵcmp: i0.ɵɵComponentDeclaration<SparkPushToggleComponent, "spark-push-toggle", never, {}, {}, never, never, true, never>;
149
+ }
150
+ /**
151
+ * The notification bell for an app's top bar (`docs/seo_analyzer_PRD.md` X10): the unread count as a
152
+ * badge, live; a menu with the latest notifications (title, message, relative time), "Mark all as read",
153
+ * a link to the full list and, where this browser can receive it, the Web Push opt-in. Choosing a
154
+ * notification marks it read and follows its link.
155
+ */
156
+ export declare class SparkNotificationBellComponent {
157
+ protected readonly notifications: SparkNotificationsService;
158
+ private readonly language;
159
+ private readonly options;
160
+ protected readonly colors: typeof Color;
161
+ /** Where "All notifications" goes. Default: `provideSparkNotifications({ listLink })`, else the `notifications` query page. */
162
+ readonly allLink: import("@angular/core").InputSignal<string | readonly unknown[] | null>;
163
+ readonly open: import("@angular/core").WritableSignal<boolean>;
164
+ protected readonly listLink: import("@angular/core").Signal<string | readonly unknown[]>;
165
+ protected readonly countText: import("@angular/core").Signal<string>;
166
+ protected readonly ariaLabel: import("@angular/core").Signal<string>;
167
+ readonly positions: ConnectedPosition[];
168
+ protected t(key: string): string;
169
+ protected when(notification: SparkNotification): string;
170
+ close(): void;
171
+ /** A click outside closes the menu, except on the bell itself, whose own handler toggles it. */
172
+ onOutsideClick(event: MouseEvent, toggle: HTMLElement): void;
173
+ choose(notification: SparkNotification): Promise<void>;
174
+ markAllRead(): Promise<void>;
175
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkNotificationBellComponent, never>;
176
+ static ɵcmp: i0.ɵɵComponentDeclaration<SparkNotificationBellComponent, "spark-notification-bell", never, {
177
+ "allLink": {
178
+ "alias": "allLink";
179
+ "required": false;
180
+ "isSignal": true;
181
+ };
182
+ }, {}, never, never, true, never>;
183
+ }
184
+ /**
185
+ * `date` relative to `now` in `language` ("5 minutes ago", "il y a 5 minutes", "5 minuten geleden"),
186
+ * through `Intl.RelativeTimeFormat`; "now" under a minute. Empty for a missing date.
187
+ */
188
+ export declare function sparkRelativeTime(date: Date | null | undefined, now: number, language: string): string;
@@ -0,0 +1,40 @@
1
+ import { Color } from "@mintplayer/ng-bootstrap";
2
+ import * as i0 from "@angular/core";
3
+ /** What `POST /spark/organizations/accept` answers. */
4
+ export interface SparkOrganizationAcceptance {
5
+ accepted: boolean;
6
+ organizationId: string | null;
7
+ organization: string | null;
8
+ problem: string | null;
9
+ /** Which refusal: `invalid` (any unknown, used or expired link) or `otherAddress` (D43). */
10
+ reason?: 'invalid' | 'otherAddress' | null;
11
+ }
12
+ /**
13
+ * The target of a mailed organization invitation or join request
14
+ * (`/organizations/accept/:token?id=<invitationId>`, `docs/seo_analyzer_PRD.md` D18). Accepting is an
15
+ * explicit click, never a side effect of opening the link (mail scanners open links). An invitation
16
+ * makes the signed-in account a member; a join request lets its requester in and needs an Owner or
17
+ * Admin. Every failure reads the same, except a valid invitation opened by an account that does not own
18
+ * the invited address (D43): the page says so, so the user can sign in with the right account.
19
+ */
20
+ export declare class SparkAcceptOrganizationInvitationComponent {
21
+ private readonly http;
22
+ private readonly route;
23
+ private readonly translations;
24
+ protected readonly colors: typeof Color;
25
+ private readonly token;
26
+ private readonly invitationId;
27
+ /** A link without both values cannot be an invitation. */
28
+ protected readonly valid: boolean;
29
+ readonly busy: import("@angular/core").WritableSignal<boolean>;
30
+ readonly failed: import("@angular/core").WritableSignal<boolean>;
31
+ /** The refusal to show: a translation key under `organizations.spa`. */
32
+ readonly reason: import("@angular/core").WritableSignal<"invalid" | "otherAddress">;
33
+ /** The organization joined, once accepted. */
34
+ readonly joined: import("@angular/core").WritableSignal<SparkOrganizationAcceptance | null>;
35
+ /** The page's texts: `organizations.spa.*` in the organizations library's `translations.json`. */
36
+ protected t(key: string): string;
37
+ accept(): Promise<void>;
38
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkAcceptOrganizationInvitationComponent, never>;
39
+ static ɵcmp: i0.ɵɵComponentDeclaration<SparkAcceptOrganizationInvitationComponent, "spark-accept-organization-invitation", never, {}, {}, never, never, true, never>;
40
+ }
@@ -0,0 +1,72 @@
1
+ import { CanActivateFn } from "@angular/router";
2
+ import { SparkAuthRouteEntry } from "@mintplayer/ng-spark/auth/models";
3
+ import { SparkAuthRoutesFeature } from "@mintplayer/ng-spark/auth/routes";
4
+ import * as i0 from "@angular/core";
5
+ import { OnInit } from "@angular/core";
6
+ /**
7
+ * The organizations package's one SPA page (`docs/seo_analyzer_PRD.md` X11): the target of a mailed
8
+ * invitation or join request, `organizations/accept/:token?id=<invitationId>`, as a `sparkAuthRoutes()`
9
+ * feature:
10
+ *
11
+ * ```ts
12
+ * sparkAuthRoutes(withExternalLogin(), withAccount(), withOrganizationRoutes())
13
+ * ```
14
+ *
15
+ * The page needs a signed-in user: the guard (default `[sparkAuthGuard]`) sends everyone else to the
16
+ * sign-in page and back. Organizations, members and invitations themselves are the generic `po/:type`
17
+ * and `query/:id` screens. Moving the page means setting `Spark:Organizations:AcceptPath` to match, so
18
+ * the mailed links follow.
19
+ */
20
+ export declare function withOrganizationRoutes(options?: {
21
+ canActivate?: CanActivateFn[];
22
+ accept?: SparkAuthRouteEntry;
23
+ }): SparkAuthRoutesFeature;
24
+ /** One organization the switcher offers (`GET /spark/organizations/current`). */
25
+ export interface SparkOrganizationChoice {
26
+ id: string;
27
+ name: string;
28
+ isPersonal: boolean;
29
+ }
30
+ /** What `/spark/organizations/current` answers: the chosen organization (`null`: all of them) and the caller's organizations. */
31
+ export interface SparkCurrentOrganization {
32
+ current: string | null;
33
+ organizations: SparkOrganizationChoice[];
34
+ }
35
+ /** English fallbacks of the switcher's texts (`organizations.spa.*` in the organizations package's translations). */
36
+ export declare const SPARK_ORGANIZATION_SWITCHER_TEXTS: Record<string, string>;
37
+ /**
38
+ * The organization switcher for an app's top bar (`docs/seo_analyzer_PRD.md` Q-M11a): "All organizations" or one
39
+ * of the caller's organizations. The server keeps the choice (a cookie it checks against the caller's memberships
40
+ * on every request), lists of organization-owned rows show only the chosen organization's, and a new row whose
41
+ * form names no organization lands in it. A detail page, a sub-query and the organization list are not narrowed.
42
+ *
43
+ * ```html
44
+ * @if (auth.isAuthenticated()) { <spark-organization-switcher /> }
45
+ * ```
46
+ *
47
+ * Renders nothing for a user in one organization (or none), so it can sit in the top bar unconditionally. A
48
+ * choice reloads the page (`reloadOnChange`), so every grid, live list and cached page reads the new scope.
49
+ */
50
+ export declare class SparkOrganizationSwitcherComponent implements OnInit {
51
+ private readonly http;
52
+ private readonly language;
53
+ private readonly document;
54
+ /** Reload the page after a choice, so every list reads the new scope. Default true. */
55
+ readonly reloadOnChange: import("@angular/core").InputSignal<boolean>;
56
+ readonly state: import("@angular/core").WritableSignal<SparkCurrentOrganization | null>;
57
+ readonly busy: import("@angular/core").WritableSignal<boolean>;
58
+ ngOnInit(): Promise<void>;
59
+ protected t(key: string): string;
60
+ protected label(organization: SparkOrganizationChoice): string;
61
+ /** Reloads the page; a seam for tests. */
62
+ protected reload(): void;
63
+ choose(organizationId: string | null): Promise<void>;
64
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkOrganizationSwitcherComponent, never>;
65
+ static ɵcmp: i0.ɵɵComponentDeclaration<SparkOrganizationSwitcherComponent, "spark-organization-switcher", never, {
66
+ "reloadOnChange": {
67
+ "alias": "reloadOnChange";
68
+ "required": false;
69
+ "isSignal": true;
70
+ };
71
+ }, {}, never, never, true, never>;
72
+ }
@@ -119,6 +119,22 @@ export interface SparkQueryRowAction {
119
119
  export declare const SPARK_QUERY_ROW_ACTIONS: InjectionToken<SparkQueryRowAction[]>;
120
120
  /** Registers grid row actions (multi — every call adds). */
121
121
  export declare function provideSparkQueryRowActions(...actions: SparkQueryRowAction[]): Provider[];
122
+ /**
123
+ * A layout that draws a detail page's attributes in place of the tabs and cards (#504), for objects it
124
+ * `matches`: Dashboards' tile layout for objects carrying `metadata.dashboard`. It must declare
125
+ * `context = input.required<SparkDetailContext>()`. The action bar, sub-queries, the extra content
126
+ * template and the detail panels stay; the first matching layout wins.
127
+ */
128
+ export interface SparkDetailLayout {
129
+ id: string;
130
+ component: Type<unknown>;
131
+ /** Whether this layout draws `item`. Called on every change of the object, so keep it cheap. */
132
+ matches(item: PersistentObject, entityType: EntityType): boolean;
133
+ }
134
+ /** Multi-provider: layouts that replace the attribute cards of the routed detail page for the objects they match. */
135
+ export declare const SPARK_DETAIL_LAYOUTS: InjectionToken<SparkDetailLayout[]>;
136
+ /** Registers detail layouts (multi — every call adds). */
137
+ export declare function provideSparkDetailLayouts(...layouts: SparkDetailLayout[]): Provider[];
122
138
  /** Multi-provider: panels under the attributes of the routed detail page. */
123
139
  export declare const SPARK_DETAIL_PANELS: InjectionToken<SparkDetailPanel[]>;
124
140
  /** Multi-provider: extra buttons in the routed detail page's action bar. */
@@ -46,6 +46,16 @@ export declare class AttributeValuePipe implements PipeTransform {
46
46
  static ɵfac: i0.ɵɵFactoryDeclaration<AttributeValuePipe, never>;
47
47
  static ɵpipe: i0.ɵɵPipeDeclaration<AttributeValuePipe, "attributeValue", true>;
48
48
  }
49
+ /**
50
+ * A displayed value, or `-` when there is none: `null`, `undefined` or an empty (or blank) string. Unlike
51
+ * `value || '-'` it keeps the falsy values that are data: the number `0` (a page at depth 0, a count of 0) and `false`.
52
+ */
53
+ export declare function sparkOrDash(value: unknown): unknown;
54
+ export declare class OrDashPipe implements PipeTransform {
55
+ transform(value: unknown): unknown;
56
+ static ɵfac: i0.ɵɵFactoryDeclaration<OrDashPipe, never>;
57
+ static ɵpipe: i0.ɵɵPipeDeclaration<OrDashPipe, "orDash", true>;
58
+ }
49
59
  export declare class RawAttributeValuePipe implements PipeTransform {
50
60
  transform(attrName: string, item: PersistentObject | null): any;
51
61
  static ɵfac: i0.ɵɵFactoryDeclaration<RawAttributeValuePipe, never>;
@@ -12,6 +12,7 @@ export declare class SparkPoDetailComponent {
12
12
  private readonly returnNavigation;
13
13
  private readonly queryRefresh;
14
14
  private readonly attributeRefresh;
15
+ private readonly live;
15
16
  protected readonly lang: SparkLanguageService;
16
17
  private readonly clientMethods;
17
18
  protected readonly rendererRegistry: import("@mintplayer/ng-spark/renderers").SparkAttributeRendererRegistration[];
@@ -22,6 +23,7 @@ export declare class SparkPoDetailComponent {
22
23
  */
23
24
  protected readonly detailPanels: import("@mintplayer/ng-spark/panels").SparkDetailPanel[];
24
25
  protected readonly detailActions: import("@mintplayer/ng-spark/panels").SparkDetailAction[];
26
+ private readonly detailLayouts;
25
27
  showCustomActions: import("@angular/core").InputSignal<boolean>;
26
28
  extraActionsTemplate: import("@angular/core").InputSignal<TemplateRef<void> | null>;
27
29
  extraContentTemplate: import("@angular/core").InputSignal<TemplateRef<{
@@ -45,12 +47,17 @@ export declare class SparkPoDetailComponent {
45
47
  queryActionsTemplate: import("@angular/core").InputSignal<TemplateRef<any> | null>;
46
48
  edited: import("@angular/core").OutputEmitterRef<void>;
47
49
  deleted: import("@angular/core").OutputEmitterRef<void>;
50
+ /** After an action succeeded; `result` is what it handed to `CustomActionArgs.SetResult` (a download url, a new id). */
48
51
  customActionExecuted: import("@angular/core").OutputEmitterRef<{
49
52
  action: CustomActionDefinition;
50
53
  item: PersistentObject;
54
+ result?: unknown;
51
55
  }>;
52
56
  colors: typeof Color;
57
+ /** The page could not be loaded: shown instead of the page. */
53
58
  errorMessage: import("@angular/core").WritableSignal<string | null>;
59
+ /** A custom action failed or was refused: shown above the page, which stays (it used to replace the page). */
60
+ actionError: import("@angular/core").WritableSignal<string | null>;
54
61
  entityType: import("@angular/core").WritableSignal<EntityType | null>;
55
62
  /** The sub-queries rendered as cards below the form, in object form (#460, D17). */
56
63
  protected readonly subQueries: import("@angular/core").Signal<import("@mintplayer/ng-spark/models").SparkSubQuery[]>;
@@ -80,6 +87,8 @@ export declare class SparkPoDetailComponent {
80
87
  protected readonly isDeletedView: import("@angular/core").Signal<boolean>;
81
88
  /** What every add-on panel and action receives; rebuilt whenever the object or rights change. */
82
89
  protected readonly detailContext: import("@angular/core").Signal<SparkDetailContext | null>;
90
+ /** The registered layout that draws this object's attributes instead of the tabs (#504), if one matches. */
91
+ protected readonly detailLayout: import("@angular/core").Signal<import("@mintplayer/ng-spark/panels").SparkDetailLayout | null>;
83
92
  /**
84
93
  * The actions actually offered for the object on screen.
85
94
  *
@@ -96,6 +105,21 @@ export declare class SparkPoDetailComponent {
96
105
  */
97
106
  visibleCustomActions: import("@angular/core").Signal<CustomActionDefinition[]>;
98
107
  constructor();
108
+ /** The live subscription of the object on screen, while its type is live; keyed so a reload keeps it. */
109
+ private liveSubscription;
110
+ private liveKey;
111
+ /** The catalogue and rights of the last load, so a pushed object re-derives its Edit/Delete/actions. */
112
+ private loadedActions;
113
+ /**
114
+ * Follows the object when its type is live: every push replaces the shown object and re-derives Edit,
115
+ * Delete and the custom actions from its own `can`/`disabledActions`, as a load does. `gone` (deleted, or
116
+ * no longer readable — one answer) shows the unavailable state. A subscription `error` stops following
117
+ * and keeps the loaded object: the page still shows what the HTTP load returned.
118
+ */
119
+ private follow;
120
+ private stopFollowing;
121
+ /** The translation, or `fallback` while the key has none (`t` echoes a missing key). */
122
+ private translateOr;
99
123
  /**
100
124
  * Applies patched attribute values in place, without a re-fetch.
101
125
  *
@@ -109,6 +133,11 @@ export declare class SparkPoDetailComponent {
109
133
  private loadItem;
110
134
  /** (Re)loads the object, its type, rights and actions. Also what add-ons call via `context.reload()`. */
111
135
  load(): Promise<void>;
136
+ /**
137
+ * Edit, Delete and the custom actions for `item`, from the last load's catalogue and rights: on a load,
138
+ * and again on every live push, since `can` and `disabledActions` travel with the object.
139
+ */
140
+ private deriveActions;
112
141
  /**
113
142
  * Drawn from the loaded object's own `showedOn`, falling back to the model's (#264, G-Q3): an action sets
114
143
  * it per object in `OnLoadAsync` — a stolen car's police report number is shown, the model says `None`.
@@ -133,6 +133,7 @@ export declare class SparkPoEditComponent {
133
133
  private readonly route;
134
134
  private readonly router;
135
135
  private readonly sparkService;
136
+ private readonly live;
136
137
  private readonly language;
137
138
  /** `SparkConfig.conflictDialog.showChangedBy`: whether the conflict notice may name a user at all. */
138
139
  private readonly showChangedBy;
@@ -186,6 +187,36 @@ export declare class SparkPoEditComponent {
186
187
  private readonly form;
187
188
  private readonly locale;
188
189
  constructor();
190
+ private liveSubscription;
191
+ /** The newest version pushed while this form is open; considered whenever the form can take it. */
192
+ private latestPushed;
193
+ /**
194
+ * A newer version someone else saved while the user has unsaved edits: the form is left alone and a
195
+ * banner offers to review it. Null when there is none (or it has been adopted or reviewed).
196
+ */
197
+ readonly liveTheirs: import("@angular/core").WritableSignal<PersistentObject | null>;
198
+ protected readonly liveChangedText: import("@angular/core").Signal<string>;
199
+ protected readonly liveReviewText: import("@angular/core").Signal<string>;
200
+ /**
201
+ * Watches the object while the form is open, when its type is live. A newer version (another etag) is
202
+ * adopted silently while the form is untouched — base and form replaced, so the next save posts the new
203
+ * etag — and only offered (banner, then the three-way merge) once the user has edited. `gone` reads as a
204
+ * 409 `deleted`. The 409 on save stays the server-side guarantee; this only brings it forward.
205
+ */
206
+ private follow;
207
+ private stopFollowing;
208
+ /** Adopts the latest pushed version when the form is clean, or offers it when the form is dirty. */
209
+ private considerLivePush;
210
+ /** Whether the form holds edits: it differs from the object it was built from. */
211
+ isDirty(): boolean;
212
+ /**
213
+ * The banner's Review: the existing three-way merge of the loaded base, the user's edits and the pushed
214
+ * version. Nothing conflicts → applied, and their etag adopted; conflicts → the conflict dialog. Nothing
215
+ * is saved either way.
216
+ */
217
+ reviewLiveChange(): void;
218
+ /** The translation, or `fallback` while the key has none (`t` echoes a missing key). */
219
+ private translateOr;
189
220
  private onParamsChange;
190
221
  initFormData(): void;
191
222
  /**
@@ -239,6 +270,12 @@ export declare class SparkPoEditComponent {
239
270
  * reviews and saves, and the server's validation and business rules run again on the result.
240
271
  */
241
272
  private resolveConcurrencyConflict;
273
+ /**
274
+ * Merges this form onto `theirs`, three-way against the object as loaded: disjoint edits are applied
275
+ * and their etag adopted (with a notice); true conflicts open the dialog. Shared by the 409 path and
276
+ * the live banner's Review.
277
+ */
278
+ private mergeWith;
242
279
  /**
243
280
  * Reference labels for the dialog, by id. The form keeps only ids; the labels are on the two reads
244
281
  * (theirs first: it is the fresher) and, for a reference the user just picked, in the candidate
@@ -1,6 +1,7 @@
1
1
  import * as i0 from "@angular/core";
2
2
  import { EnvironmentProviders, InjectionToken } from "@angular/core";
3
3
  import { SwRegistrationOptions } from "@angular/service-worker";
4
+ import { Location } from "@angular/common";
4
5
  export interface SparkServiceWorkerOptions {
5
6
  /** Whether the worker registers. Default: `!isDevMode()`, so only production builds register it. */
6
7
  enabled?: boolean;
@@ -11,7 +12,8 @@ export interface SparkServiceWorkerOptions {
11
12
  }
12
13
  /**
13
14
  * Registers Angular's service worker with Spark's update policy (#464, Q1b); see
14
- * {@link SparkServiceWorkerUpdates} for what happens when a new version is deployed.
15
+ * {@link SparkServiceWorkerUpdates} for what happens when a new version is deployed, and
16
+ * {@link SparkNotificationClicks} for a push notification clicked while the application is open.
15
17
  *
16
18
  * The application also needs an `ngsw-config.json` with Spark's navigation exclusions and
17
19
  * `serviceWorker` in its production build configuration; see `docs/guide-pwa.md`.
@@ -86,4 +88,48 @@ export declare class SparkServiceWorkerUpdates {
86
88
  private writeReloadedFor;
87
89
  static ɵfac: i0.ɵɵFactoryDeclaration<SparkServiceWorkerUpdates, never>;
88
90
  static ɵprov: i0.ɵɵInjectableDeclaration<any>;
89
- }
91
+ }
92
+ /**
93
+ * Notification clicks while the application is open (`docs/seo_analyzer_PRD.md` X10, D30): a click on
94
+ * a push whose `notification.data.url` is on this origin becomes an in-app router navigation, so the
95
+ * page keeps its state instead of loading again.
96
+ *
97
+ * What it does NOT do, because Angular's service worker (ngsw) already does it: show the push, and
98
+ * open the page when the application is closed. The server sends Angular's payload,
99
+ * `{ notification: { title, body, tag, data: { url, notificationId, onActionClick: { default: {
100
+ * operation, url } } } } }`, and ngsw performs `onActionClick.default` itself on the click, then
101
+ * broadcasts the click to the open pages, which is what {@link SwPush.notificationClicks} reports here.
102
+ *
103
+ * ⚠️ The operation decides whether this handler matters. ngsw runs it BEFORE the broadcast:
104
+ * `navigateLastFocusedOrOpen` makes the focused page load the URL in full, so this handler then
105
+ * finds the router already there and does nothing; `focusLastFocusedOrOpen` focuses the open page
106
+ * without loading it (and opens the URL when no page is open), leaving the navigation to this handler.
107
+ *
108
+ * Active only in a browser with the service worker enabled; started by `provideSparkServiceWorker()`.
109
+ */
110
+ export declare class SparkNotificationClicks {
111
+ private readonly swPush;
112
+ private readonly router;
113
+ private readonly location;
114
+ private readonly document;
115
+ private readonly platformId;
116
+ private readonly destroyRef;
117
+ private started;
118
+ /** Starts listening. Idempotent; called by the app initializer `provideSparkServiceWorker()` adds. */
119
+ start(): void;
120
+ private navigate;
121
+ /** A full page load (overridden in specs: jsdom cannot navigate). */
122
+ protected assign(href: string): void;
123
+ static ɵfac: i0.ɵɵFactoryDeclaration<SparkNotificationClicks, never>;
124
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
125
+ }
126
+ /**
127
+ * The router URL for `url` when it points into this application, else `null`.
128
+ *
129
+ * `url` may be app-relative (`/runs/42`) or absolute. It is resolved against the document's base URI;
130
+ * only an `http(s)` URL on the page's own origin is in-app, and its path loses the `<base href>`
131
+ * (through `Location.normalize`) because the router works below it. Everything else (another origin,
132
+ * `javascript:`, `mailto:`, an unparseable value) is `null`, so a caller never hands data-borne URLs
133
+ * to the router or to `location.assign` unchecked.
134
+ */
135
+ export declare function sparkInAppUrl(url: string | null | undefined, document: Document, location: Location | null): string | null;
@@ -3,7 +3,7 @@ import { TemplateRef } from "@angular/core";
3
3
  import { Color } from "@mintplayer/ng-bootstrap";
4
4
  import { SparkLanguageService } from "@mintplayer/ng-spark/services";
5
5
  import { SparkQueryToolbarAction } from "@mintplayer/ng-spark/grid";
6
- import { CustomActionDefinition, QueryColumn, QueryColumnFilter, QueryResultItem, SparkDeletedFilter, SparkSelectionModeSetting } from "@mintplayer/ng-spark/models";
6
+ import { CustomActionDefinition, QueryColumnFilter, QueryResultItem, SparkDeletedFilter, SparkSelectionModeSetting } from "@mintplayer/ng-spark/models";
7
7
  import { SparkQueryListContext } from "@mintplayer/ng-spark/panels";
8
8
  /**
9
9
  * The routed query page: chrome around one {@link SparkQueryGridComponent}.
@@ -14,9 +14,11 @@ import { SparkQueryListContext } from "@mintplayer/ng-spark/panels";
14
14
  * routes — `query/:queryId`, and `po/:type`, which resolves an entity type to a query. That
15
15
  * second one is type-to-query resolution, not query rendering, and is why this component still
16
16
  * exists rather than the router pointing at the grid.
17
- * - **Streaming.** The websocket lives here so it stays out of every PO detail page's bundle;
18
- * the snapshot is filtered and sorted client-side and handed to the grid as `[data]`.
19
- * - The action bar, the caption, the LIVE badge, the search box and the New button.
17
+ * - The action bar, the caption, the LIVE badge (the grid's `isLive()`), the search box and the
18
+ * New button.
19
+ *
20
+ * Live updates are the grid's (X4): a live query is followed by `spark-query-grid` itself, with
21
+ * server-side paging, sort and search, so every embedded grid follows it too — not only this page.
20
22
  *
21
23
  * The grid itself — columns, cells, paging, the row link, selection, custom-action execution —
22
24
  * is the shared component. This page previously wrote out `<bs-datatable>` twice, once per
@@ -26,8 +28,6 @@ export declare class SparkQueryListComponent {
26
28
  private readonly route;
27
29
  private readonly router;
28
30
  private readonly sparkService;
29
- private readonly streamingService;
30
- private readonly destroyRef;
31
31
  protected readonly lang: SparkLanguageService;
32
32
  extraActionsTemplate: import("@angular/core").InputSignal<TemplateRef<void> | null>;
33
33
  showCustomActions: import("@angular/core").InputSignal<boolean>;
@@ -65,6 +65,8 @@ export declare class SparkQueryListComponent {
65
65
  protected readonly resultCount: import("@angular/core").Signal<number | null>;
66
66
  protected readonly isVirtualScrolling: import("@angular/core").Signal<boolean>;
67
67
  protected readonly gridError: import("@angular/core").Signal<string | null>;
68
+ /** Whether the grid follows the query over `/spark/live` right now: the LIVE badge. */
69
+ protected readonly isLive: import("@angular/core").Signal<boolean>;
68
70
  protected readonly permissions: import("@angular/core").Signal<import("@mintplayer/ng-spark/models").EntityPermissions | null>;
69
71
  /** Add-on buttons for the action bar (#460, `SPARK_QUERY_LIST_ACTIONS`), e.g. the Deleted toggle. */
70
72
  protected readonly listActions: import("@mintplayer/ng-spark/panels").SparkQueryListAction[];
@@ -89,19 +91,6 @@ export declare class SparkQueryListComponent {
89
91
  protected toolbarActionUnavailableReason(action: SparkQueryToolbarAction): string | null;
90
92
  protected runToolbarAction(action: SparkQueryToolbarAction): void;
91
93
  protected isActionEnabled(action: CustomActionDefinition): boolean;
92
- isStreaming: import("@angular/core").WritableSignal<boolean>;
93
- private streamingSub;
94
- /** Columns as sent with the stream's snapshot; empty until it arrives. */
95
- protected readonly streamColumns: import("@angular/core").WritableSignal<QueryColumn[]>;
96
- private readonly allItems;
97
- private readonly streamItems;
98
- /**
99
- * Rows handed to the grid, or `null` to let it fetch for itself.
100
- *
101
- * Null for a normal query — an empty array would read as "here are no rows" and suppress the
102
- * fetch entirely.
103
- */
104
- protected readonly gridData: import("@angular/core").Signal<QueryResultItem[] | null>;
105
94
  constructor();
106
95
  /**
107
96
  * The parent the URL scopes the query to (`?parentId=…&parentType=…`), sent exactly as a sub-query
@@ -138,10 +127,6 @@ export declare class SparkQueryListComponent {
138
127
  protected onCustomAction(action: CustomActionDefinition): Promise<void>;
139
128
  protected onCreate(): void;
140
129
  protected clearSearch(): void;
141
- private connectStreaming;
142
- private disconnectStreaming;
143
- private handleStreamingMessage;
144
- private applyFilter;
145
130
  static ɵfac: i0.ɵɵFactoryDeclaration<SparkQueryListComponent, never>;
146
131
  static ɵcmp: i0.ɵɵComponentDeclaration<SparkQueryListComponent, "spark-query-list", never, {
147
132
  "extraActionsTemplate": {