@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mintplayer/ng-spark",
3
3
  "private": false,
4
- "version": "22.31.0",
4
+ "version": "22.33.0",
5
5
  "description": "Angular component library for MintPlayer.Spark CRUD applications",
6
6
  "repository": {
7
7
  "type": "git",
@@ -160,6 +160,18 @@
160
160
  "types": "./types/mintplayer-ng-spark-moderation.d.ts",
161
161
  "default": "./fesm2022/mintplayer-ng-spark-moderation.mjs"
162
162
  },
163
+ "./notifications": {
164
+ "types": "./types/mintplayer-ng-spark-notifications.d.ts",
165
+ "default": "./fesm2022/mintplayer-ng-spark-notifications.mjs"
166
+ },
167
+ "./organizations": {
168
+ "types": "./types/mintplayer-ng-spark-organizations.d.ts",
169
+ "default": "./fesm2022/mintplayer-ng-spark-organizations.mjs"
170
+ },
171
+ "./organizations/accept": {
172
+ "types": "./types/mintplayer-ng-spark-organizations-accept.d.ts",
173
+ "default": "./fesm2022/mintplayer-ng-spark-organizations-accept.mjs"
174
+ },
163
175
  "./panels": {
164
176
  "types": "./types/mintplayer-ng-spark-panels.d.ts",
165
177
  "default": "./fesm2022/mintplayer-ng-spark-panels.mjs"
@@ -204,10 +216,22 @@
204
216
  "types": "./types/mintplayer-ng-spark-routes.d.ts",
205
217
  "default": "./fesm2022/mintplayer-ng-spark-routes.mjs"
206
218
  },
219
+ "./score-renderers": {
220
+ "types": "./types/mintplayer-ng-spark-score-renderers.d.ts",
221
+ "default": "./fesm2022/mintplayer-ng-spark-score-renderers.mjs"
222
+ },
207
223
  "./services": {
208
224
  "types": "./types/mintplayer-ng-spark-services.d.ts",
209
225
  "default": "./fesm2022/mintplayer-ng-spark-services.mjs"
210
226
  },
227
+ "./share": {
228
+ "types": "./types/mintplayer-ng-spark-share.d.ts",
229
+ "default": "./fesm2022/mintplayer-ng-spark-share.mjs"
230
+ },
231
+ "./share/page": {
232
+ "types": "./types/mintplayer-ng-spark-share-page.d.ts",
233
+ "default": "./fesm2022/mintplayer-ng-spark-share-page.mjs"
234
+ },
211
235
  "./shell": {
212
236
  "types": "./types/mintplayer-ng-spark-shell.d.ts",
213
237
  "default": "./fesm2022/mintplayer-ng-spark-shell.mjs"
@@ -219,23 +219,22 @@ export declare function sparkActionClass(definition: CustomActionDefinition, siz
219
219
  * The one Spark grid: a `<bs-datatable>` rendering a query or a sub-query.
220
220
  *
221
221
  * This replaces two components that were the same grid written twice — and, counting
222
- * `spark-query-list`'s streaming and paged branches, the `<bs-datatable>` element itself written
223
- * three times. The duplication had already produced four user-visible bugs, each fixed on one
224
- * side and not the other: `[indeterminate]` on null booleans, permission state surviving a failed
222
+ * `spark-query-list`'s former streaming and paged branches, the `<bs-datatable>` element itself
223
+ * written three times. The duplication had already produced four user-visible bugs, each fixed on
224
+ * one side and not the other: `[indeterminate]` on null booleans, permission state surviving a failed
225
225
  * reload, a swallowed fetch failure, and virtual-scroll sizing. `SparkGridRenderers` was
226
226
  * extracted to stop that and could only take the stateless helpers with it; this takes the rest.
227
227
  *
228
- * ## What it deliberately does NOT own: streaming
228
+ * ## Where the rows come from
229
229
  *
230
- * `spark-grid-renderers.ts` records why — a websocket dependency graph must not reach the bundle
231
- * of every PO detail page, none of which stream. So rows can come from **either** side:
232
- *
233
- * - unbound `data` — the grid fetches for itself, paging and sorting server-side;
230
+ * - unbound `data` — the grid fetches for itself, paging and sorting server-side: over HTTP, or over
231
+ * `/spark/live` when the grid is live (see {@link live});
234
232
  * - bound `data` — the grid renders what it is given and never fetches.
235
233
  *
236
- * `spark-query-list` keeps the socket and feeds its snapshot in. This is the line `bs-datatable`
237
- * itself draws: `[data]` and `[fetch]` are mutually exclusive, and binding both makes `[fetch]`
238
- * win silently.
234
+ * This is the line `bs-datatable` itself draws: `[data]` and `[fetch]` are mutually exclusive, and
235
+ * binding both makes `[fetch]` win silently. Live rides on `[fetch]`, so server-side paging, sort,
236
+ * search and filters work the same live or not; every embedded grid, sub-query and card follows a live
237
+ * query, not only the query page.
239
238
  *
240
239
  * ## Reading its state
241
240
  *
@@ -246,6 +245,7 @@ export declare function sparkActionClass(definition: CustomActionDefinition, siz
246
245
  */
247
246
  export declare class SparkQueryGridComponent {
248
247
  private readonly sparkService;
248
+ private readonly liveService;
249
249
  private readonly gridRenderers;
250
250
  private readonly queryRefresh;
251
251
  private readonly injector;
@@ -282,11 +282,31 @@ export declare class SparkQueryGridComponent {
282
282
  /**
283
283
  * Rows supplied from outside. When bound, the grid renders these and runs no fetch.
284
284
  *
285
- * This is how streaming stays out of this component (see the class comment). `null` — the
286
- * default — means "fetch for yourself"; an empty array means "you have been given no rows",
287
- * which is a different statement and renders an empty grid rather than triggering a load.
285
+ * `null` — the default — means "fetch for yourself"; an empty array means "you have been given no
286
+ * rows", which is a different statement and renders an empty grid rather than triggering a load.
287
+ * Bound rows are never live.
288
288
  */
289
289
  data: import("@angular/core").InputSignal<QueryResultItem[] | null>;
290
+ /**
291
+ * Whether this grid follows its query over `/spark/live` (X4): `undefined` — the default — follows the
292
+ * query's own `live` flag; `true`/`false` overrides it (`[live]="false"` on a dashboard tile).
293
+ *
294
+ * Live, the grid's current request (page, sort, search, column filters, parent, deleted modes — exactly
295
+ * the `/execute` body) is ONE server subscription, sent again as an `update` on every page, sort, search
296
+ * or filter change, and every pushed page is rendered as it arrives: rows that change, appear or leave.
297
+ * A subscription `error` clears the rows and shows the message; it is not retried (see {@link isLive}).
298
+ * A virtual-scrolling query is never live: its blocks are separate pages fetched as the user scrolls, and
299
+ * one subscription follows one page.
300
+ */
301
+ live: import("@angular/core").InputSignal<boolean | undefined>;
302
+ /**
303
+ * The live subscription ended with an error for the query loaded now. Reset by a new load; a live grid
304
+ * does not resubscribe by itself after an error (the answer would not change).
305
+ */
306
+ private readonly liveFailed;
307
+ /** Whether rows arrive over `/spark/live` right now: the query page shows its LIVE badge from this. */
308
+ readonly isLive: import("@angular/core").Signal<boolean>;
309
+ private liveWanted;
290
310
  /**
291
311
  * Server-side search term, passed through to the query.
292
312
  *
@@ -539,10 +559,9 @@ export declare class SparkQueryGridComponent {
539
559
  deleteRows(rows: QueryResultItem[], definition?: CustomActionDefinition): Promise<void>;
540
560
  isVirtualScrolling: import("@angular/core").Signal<boolean>;
541
561
  /**
542
- * Columns as sent with the most recent fetched page.
562
+ * Columns as sent with the most recent fetched (or pushed) page.
543
563
  *
544
- * A host binding `data` (streaming) supplies its own via the `columns` input instead — the
545
- * snapshot carries them once, exactly as a paged result does.
564
+ * A host binding `data` supplies its own via the `columns` input instead.
546
565
  */
547
566
  private readonly fetchedColumns;
548
567
  /**
@@ -557,15 +576,7 @@ export declare class SparkQueryGridComponent {
557
576
  * each string names comes from `bsDatatableColumnLabel` in the template. See `sparkDatatableLabels`.
558
577
  */
559
578
  datatableLabels: import("@angular/core").Signal<import("@mintplayer/ng-spark/services").SparkDatatableLabels>;
560
- /**
561
- * True when rows do not come from this component's own fetch.
562
- *
563
- * Either because the host bound `data`, or because the query streams — a streaming query's rows
564
- * arrive over a socket and `/execute` is not a way to get them. The grid recognises the second
565
- * case itself rather than waiting to be told, because it resolves the query before the host can
566
- * see it: left to `data` alone, every streaming grid would fire one pointless fetch on mount,
567
- * before the host had anything to bind.
568
- */
579
+ /** True when rows do not come from this component's own fetch: the host bound `data`. */
569
580
  hasExternalData: import("@angular/core").Signal<boolean>;
570
581
  /**
571
582
  * Whether an action can run right now: its selection rule is satisfied, and the client method it
@@ -578,6 +589,22 @@ export declare class SparkQueryGridComponent {
578
589
  */
579
590
  actionUnavailableReason(action: CustomActionDefinition): string | null;
580
591
  constructor();
592
+ /** The live subscription of the query loaded now, while the grid is live. */
593
+ private liveSession;
594
+ private disposeLive;
595
+ /**
596
+ * A pushed page: re-fetch through the datatable, which lands in the live fetch and is served from
597
+ * memory. Through `reload()` rather than a new `[fetch]` identity, so page, sort and scroll stay.
598
+ */
599
+ private onLivePush;
600
+ /**
601
+ * A live subscription error (defect 4): clear the rows — a stale page must never stay up looking current
602
+ * — and say why. Not retried: `unavailable` (unknown, forbidden, not live, missing parent — one answer),
603
+ * `invalid`, `limit` and `failed` would answer the same again, and a socket-level failure (4429, a
604
+ * repeated 4401) is the socket's to surface. No HTTP fallback either: `unavailable` is the same answer
605
+ * `/execute` gives as a 404, and an app stream's rows exist only on the socket.
606
+ */
607
+ private onLiveError;
581
608
  /**
582
609
  * Re-run the query, keeping the current page, sort and scroll position.
583
610
  *
@@ -689,7 +716,26 @@ export declare class SparkQueryGridComponent {
689
716
  * rendered correctly as a page. Both grids are this one now, so there is one answer.
690
717
  */
691
718
  private resolveEntityType;
719
+ /** The datatable's `[fetch]`: over `/spark/live` when the grid is live (see {@link live}), else HTTP. */
692
720
  private makeFetch;
721
+ /**
722
+ * The `/execute` body for one datatable request — the same for HTTP and live. Read at call time, so
723
+ * what forces a refetch is the fetch identity `reload()` creates, not the value captured when the
724
+ * closure was built.
725
+ */
726
+ private requestFor;
727
+ /**
728
+ * One page as the datatable takes it, recording what the chrome reads off a result: count, columns,
729
+ * the result's withheld actions, the ids on screen.
730
+ */
731
+ private toPage;
732
+ /**
733
+ * Live: the request goes to this grid's {@link LiveGridSession} — subscribed on the first fetch, sent as
734
+ * an `update` when it changes, answered from the latest push when it does not. After an error every fetch
735
+ * is an empty page and the error stays shown.
736
+ */
737
+ private makeLiveFetch;
738
+ private makeHttpFetch;
693
739
  private loadLookupReferenceOptions;
694
740
  /**
695
741
  * The underlying value a custom renderer receives, which is not the printable one.
@@ -730,6 +776,11 @@ export declare class SparkQueryGridComponent {
730
776
  "required": false;
731
777
  "isSignal": true;
732
778
  };
779
+ "live": {
780
+ "alias": "live";
781
+ "required": false;
782
+ "isSignal": true;
783
+ };
733
784
  "search": {
734
785
  "alias": "search";
735
786
  "required": false;
@@ -341,6 +341,12 @@ export interface EntityType {
341
341
  */
342
342
  canRead?: boolean;
343
343
  alias?: string;
344
+ /**
345
+ * Whether an object of this type is followed over `/spark/live` (X4): `spark-po-detail` replaces the
346
+ * shown object when it changes and shows the unavailable state when it is deleted, and `spark-po-edit`
347
+ * adopts a newer version (or offers a merge when the form is dirty). Opt-in per type.
348
+ */
349
+ live?: boolean;
344
350
  /**
345
351
  * Breadcrumb template: literal text plus `{AttributeName}` placeholders. A scalar placeholder
346
352
  * renders its value; a reference placeholder renders the referenced entity's breadcrumb.
@@ -409,8 +415,12 @@ export interface SparkQuery {
409
415
  indexName?: string;
410
416
  /** Optional entity type name (e.g., "Person"). When set, used for entity type resolution. */
411
417
  entityType?: string;
412
- /** When true, this query uses WebSocket streaming with snapshot + patch updates. */
413
- isStreamingQuery?: boolean;
418
+ /**
419
+ * Whether grids showing this query follow it over `/spark/live` (X4): the page re-renders when a row in
420
+ * it changes, appears or disappears, with server-side paging, sort, search and filters intact. Opt-in
421
+ * per query; a grid's `[live]` input overrides it. See `docs/guide-live-updates.md`.
422
+ */
423
+ live?: boolean;
414
424
  /**
415
425
  * Whether the grid offers row selection (#460, D17). Absent is `'auto'`: derived from the offered
416
426
  * actions. A sub-query entry on the parent's type may override it.
@@ -734,30 +744,153 @@ export interface EntityPermissions {
734
744
  canRevertContribution?: boolean;
735
745
  }
736
746
  /**
737
- * The opening message of a stream: the column metadata plus the rows so far.
747
+ * The live-updates protocol (`/spark/live`, X4), as `docs/guide-live-updates.md` "The protocol" fixes it.
738
748
  *
739
- * Columns arrive once, here. A stream is one result whose rows arrive over time, so its shape is
740
- * fixed when it opens — patches carry values only.
749
+ * One WebSocket per browser tab carries every subscription; one JSON object per text frame. Subscriptions
750
+ * are reads only: mutations stay on HTTP.
741
751
  */
742
- export interface StreamingSnapshotMessage {
743
- type: 'snapshot';
744
- columns: QueryColumn[];
745
- data: QueryResultItem[];
746
- }
747
- export interface StreamingPatchItem {
752
+ /** A subscription to one persistent object: exactly the body of `POST /spark/po/load`. */
753
+ export interface LivePoRequest {
754
+ objectTypeId: string;
748
755
  id: string;
749
- /** Changed cell values, keyed by column name. Never metadata — the snapshot fixed that. */
750
- values: Record<string, any>;
756
+ deleted?: SparkDeletedFilter | null;
757
+ }
758
+ /** A sort column as `/spark/queries/execute` takes it. */
759
+ export interface LiveSortColumn {
760
+ property: string;
761
+ direction: 'asc' | 'desc';
762
+ }
763
+ /**
764
+ * A subscription to one page of a query: exactly the body of `POST /spark/queries/execute`
765
+ * (`retryResults` is ignored by the server — a push cannot prompt).
766
+ */
767
+ export interface LiveQueryRequest {
768
+ queryId: string;
769
+ skip?: number | null;
770
+ take?: number | null;
771
+ sortColumns?: LiveSortColumn[] | null;
772
+ search?: string | null;
773
+ columns?: QueryColumnFilter[] | null;
774
+ parentId?: string | null;
775
+ parentType?: string | null;
776
+ deleted?: SparkDeletedFilter | null;
777
+ parentDeleted?: SparkDeletedFilter | null;
778
+ }
779
+ export interface LiveSubscribePoOp {
780
+ op: 'subscribe';
781
+ sub: string;
782
+ po: LivePoRequest;
783
+ }
784
+ export interface LiveSubscribeQueryOp {
785
+ op: 'subscribe';
786
+ sub: string;
787
+ query: LiveQueryRequest;
788
+ }
789
+ /** The same query subscription with a new page, sort, search or filters; the server keeps the sub. */
790
+ export interface LiveUpdateOp {
791
+ op: 'update';
792
+ sub: string;
793
+ query: LiveQueryRequest;
794
+ }
795
+ export interface LiveUnsubscribeOp {
796
+ op: 'unsubscribe';
797
+ sub: string;
751
798
  }
752
- export interface StreamingPatchMessage {
799
+ export type LiveClientOp = LiveSubscribePoOp | LiveSubscribeQueryOp | LiveUpdateOp | LiveUnsubscribeOp;
800
+ /** `error` codes. Every one ends its subscription; the socket stays open. */
801
+ export type LiveErrorCode = 'unavailable' | 'invalid' | 'limit' | 'failed';
802
+ /** The changes to a held persistent object: top-level fields (`null` = removed) and whole attributes, by `name`. */
803
+ export interface LivePoPatch {
804
+ set?: Record<string, unknown>;
805
+ attributes?: PersistentObjectAttribute[];
806
+ }
807
+ /**
808
+ * The changes to a held query page. Apply: index the held rows by `id`; drop `remove`; replace or add each
809
+ * `upsert`; the rows are `order` mapped through the index when present, otherwise the held order minus the
810
+ * removed rows; then assign `set` (`totalItems`, `skip`, `take`, `disabledActions`, `typeHints`, …).
811
+ */
812
+ export interface LiveQueryPatch {
813
+ upsert?: QueryResultItem[];
814
+ remove?: string[];
815
+ order?: string[];
816
+ set?: Record<string, unknown>;
817
+ }
818
+ /** The first state of an object, or one whose set or order of attributes changed. Replace. */
819
+ export interface LivePoFrame {
820
+ type: 'po';
821
+ sub: string;
822
+ po: PersistentObject;
823
+ }
824
+ export interface LivePoPatchFrame {
825
+ type: 'poPatch';
826
+ sub: string;
827
+ patch: LivePoPatch;
828
+ }
829
+ /** Deleted or no longer readable (one answer). The subscription has ended. */
830
+ export interface LiveGoneFrame {
831
+ type: 'gone';
832
+ sub: string;
833
+ }
834
+ /** The first page, a page whose columns changed, or a change a patch would not make smaller. Replace. */
835
+ export interface LiveResultFrame {
836
+ type: 'result';
837
+ sub: string;
838
+ result: QueryResult;
839
+ }
840
+ export interface LivePatchFrame {
753
841
  type: 'patch';
754
- updated: StreamingPatchItem[];
842
+ sub: string;
843
+ patch: LiveQueryPatch;
755
844
  }
756
- export interface StreamingErrorMessage {
845
+ /** The subscription has ended. Without `sub` it answers an unparseable frame. */
846
+ export interface LiveErrorFrame {
757
847
  type: 'error';
848
+ sub?: string;
849
+ code: LiveErrorCode;
758
850
  message: string;
759
851
  }
760
- export type StreamingMessage = StreamingSnapshotMessage | StreamingPatchMessage | StreamingErrorMessage;
852
+ /** Heartbeat. Nothing to answer. */
853
+ export interface LivePingFrame {
854
+ type: 'ping';
855
+ }
856
+ export type LiveServerFrame = LivePoFrame | LivePoPatchFrame | LiveGoneFrame | LiveResultFrame | LivePatchFrame | LiveErrorFrame | LivePingFrame;
857
+ /** Close codes with a meaning of their own; any other code is reconnected with backoff. */
858
+ export declare const LIVE_CLOSE_REAUTHENTICATE = 4401;
859
+ export declare const LIVE_CLOSE_TOO_MANY_SOCKETS = 4429;
860
+ export declare const LIVE_CLOSE_MESSAGE_TOO_LARGE = 1009;
861
+ /**
862
+ * Why a watcher's subscription ended, beyond the server's own {@link LiveErrorCode}s — the socket-level
863
+ * failures the client surfaces instead of retrying:
864
+ * - `tooManySockets`: closed with 4429; not reconnected until a new watcher asks.
865
+ * - `unauthenticated`: closed with 4401 again within seconds of reconnecting for a 4401.
866
+ * - `messageTooLarge`: closed with 1009, a client bug; not retried blindly.
867
+ */
868
+ export type LiveFailureCode = LiveErrorCode | 'tooManySockets' | 'unauthenticated' | 'messageTooLarge';
869
+ export interface LiveErrorEvent {
870
+ type: 'error';
871
+ code: LiveFailureCode;
872
+ message: string;
873
+ }
874
+ /** `po`: the object with every patch applied. `gone` and `error` end the stream (it completes after them). */
875
+ export type LivePoEvent = {
876
+ type: 'po';
877
+ po: PersistentObject;
878
+ } | {
879
+ type: 'gone';
880
+ } | LiveErrorEvent;
881
+ /** `result`: the page with every patch applied. `error` ends the stream (it completes after it). */
882
+ export type LiveQueryEvent = {
883
+ type: 'result';
884
+ result: QueryResult;
885
+ } | LiveErrorEvent;
886
+ /**
887
+ * Applies a `patch` frame to a held page, exactly as the protocol says. A new id in `upsert` that `order`
888
+ * does not place (the server always sends `order` when membership changes, so this is defensive) is
889
+ * appended rather than dropped.
890
+ */
891
+ export declare function applyLiveQueryPatch(held: QueryResult, patch: LiveQueryPatch): QueryResult;
892
+ /** Applies a `poPatch` frame to a held object: `set` assigned, changed attributes replaced by `name`. */
893
+ export declare function applyLivePoPatch(held: PersistentObject, patch: LivePoPatch): PersistentObject;
761
894
  /**
762
895
  * Conversions between the wire format for a timestamp and what an `<input type="datetime-local">`
763
896
  * or `<input type="date">` will accept.
@@ -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
+ }