@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
@@ -0,0 +1,334 @@
1
+ import { HttpClient, HttpErrorResponse } from "@angular/common/http";
2
+ import * as i0 from "@angular/core";
3
+ import { ChangeDetectionStrategy, Component, computed, inject, signal } from "@angular/core";
4
+ import { ActivatedRoute } from "@angular/router";
5
+ import { firstValueFrom } from "rxjs";
6
+ import { Color } from "@mintplayer/ng-bootstrap";
7
+ import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
8
+ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
9
+ import { SPARK_CONFIG } from "@mintplayer/ng-spark";
10
+ import { SparkGridCellComponent } from "@mintplayer/ng-spark/grid";
11
+ import { valueFor } from "@mintplayer/ng-spark/models";
12
+ import { QueryCellValuePipe, ResolveTranslationPipe } from "@mintplayer/ng-spark/pipes";
13
+ import { cellValue, rendererValue } from "@mintplayer/ng-spark/renderers";
14
+ import { SparkLanguageService } from "@mintplayer/ng-spark/services";
15
+ const TEXTS = {
16
+ notFound: "This link does not work (any more).",
17
+ rateLimited: "Too many requests. Try again in a minute.",
18
+ unavailable: "This page could not be loaded. Try again later."
19
+ };
20
+ var SparkSharedObjectComponent = class SparkSharedObjectComponent {
21
+ http = inject(HttpClient);
22
+ route = inject(ActivatedRoute);
23
+ language = inject(SparkLanguageService);
24
+ baseUrl = `${inject(SPARK_CONFIG, { optional: true })?.baseUrl ?? "/spark"}/share`;
25
+ cellPipe = new QueryCellValuePipe();
26
+ colors = Color;
27
+ type = this.route.snapshot.paramMap.get("type") ?? "";
28
+ token = this.route.snapshot.paramMap.get("token") ?? "";
29
+ id = this.route.snapshot.queryParamMap.get("id") ?? "";
30
+ state = signal("loading", ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
31
+ shared = signal(null, ...ngDevMode ? [{ debugName: "shared" }] : /* istanbul ignore next */ []);
32
+ pages = signal({}, ...ngDevMode ? [{ debugName: "pages" }] : /* istanbul ignore next */ []);
33
+ busy = signal(false, ...ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []);
34
+ notice = signal(null, ...ngDevMode ? [{ debugName: "notice" }] : /* istanbul ignore next */ []);
35
+ object = computed(() => {
36
+ const shared = this.shared();
37
+ return shared ? {
38
+ id: shared.id,
39
+ name: shared.type,
40
+ objectTypeId: shared.type,
41
+ attributes: shared.attributes
42
+ } : null;
43
+ }, ...ngDevMode ? [{ debugName: "object" }] : /* istanbul ignore next */ []);
44
+ attributeCells = computed(() => {
45
+ const object = this.object();
46
+ if (!object) return [];
47
+ return [...object.attributes].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map((attribute) => ({
48
+ attribute,
49
+ display: this.cellPipe.transform(attribute, object, {}),
50
+ value: rendererValue(attribute)
51
+ }));
52
+ }, ...ngDevMode ? [{ debugName: "attributeCells" }] : /* istanbul ignore next */ []);
53
+ queryTables = computed(() => Object.entries(this.pages()).map(([name, result]) => ({
54
+ name,
55
+ label: this.shared()?.queryLabels?.[name] ?? void 0,
56
+ result,
57
+ columns: [...result.columns ?? []].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
58
+ last: Math.min(result.skip + result.take, result.totalItems)
59
+ })), ...ngDevMode ? [{ debugName: "queryTables" }] : /* istanbul ignore next */ []);
60
+ constructor() {
61
+ this.load();
62
+ }
63
+ t(key) {
64
+ return this.language.t("share." + key, TEXTS[key]);
65
+ }
66
+ display(column, item) {
67
+ return this.cellPipe.transform(column, item, {});
68
+ }
69
+ raw(item, column) {
70
+ return cellValue(valueFor(item, column.name));
71
+ }
72
+ async load() {
73
+ if (!this.type || !this.token || !this.id) {
74
+ this.state.set("notFound");
75
+ return;
76
+ }
77
+ this.state.set("loading");
78
+ try {
79
+ const shared = await this.post("load", {});
80
+ this.shared.set({
81
+ ...shared,
82
+ attributes: shared.attributes ?? [],
83
+ queries: shared.queries ?? {}
84
+ });
85
+ this.pages.set({ ...shared.queries ?? {} });
86
+ this.notice.set(null);
87
+ this.state.set("ready");
88
+ } catch (error) {
89
+ this.fail(error);
90
+ }
91
+ }
92
+ async page(query, skip) {
93
+ const current = this.pages()[query];
94
+ if (!current || skip < 0 || this.busy()) return;
95
+ this.busy.set(true);
96
+ try {
97
+ const result = await this.post("query", {
98
+ query,
99
+ skip,
100
+ take: current.take
101
+ });
102
+ this.pages.update((pages) => ({
103
+ ...pages,
104
+ [query]: result
105
+ }));
106
+ this.notice.set(null);
107
+ } catch (error) {
108
+ this.fail(error);
109
+ } finally {
110
+ this.busy.set(false);
111
+ }
112
+ }
113
+ post(path, extra) {
114
+ return firstValueFrom(this.http.post(`${this.baseUrl}/${path}`, {
115
+ type: this.type,
116
+ id: this.id,
117
+ token: this.token,
118
+ ...extra
119
+ }));
120
+ }
121
+ fail(error) {
122
+ const status = error instanceof HttpErrorResponse ? error.status : 0;
123
+ if (status === 404) {
124
+ this.shared.set(null);
125
+ this.pages.set({});
126
+ this.notice.set(null);
127
+ this.state.set("notFound");
128
+ return;
129
+ }
130
+ this.notice.set(this.t(status === 429 ? "rateLimited" : "unavailable"));
131
+ if (this.state() === "loading") this.state.set("failed");
132
+ }
133
+ static ɵfac = i0.ɵɵngDeclareFactory({
134
+ minVersion: "12.0.0",
135
+ version: "22.2.2",
136
+ ngImport: i0,
137
+ type: SparkSharedObjectComponent,
138
+ deps: [],
139
+ target: i0.ɵɵFactoryTarget.Component
140
+ });
141
+ static ɵcmp = i0.ɵɵngDeclareComponent({
142
+ minVersion: "17.0.0",
143
+ version: "22.2.2",
144
+ type: SparkSharedObjectComponent,
145
+ isStandalone: true,
146
+ selector: "spark-shared-object",
147
+ ngImport: i0,
148
+ template: `
149
+ <div class="spark-shared-object">
150
+ @if (notice(); as text) {
151
+ <bs-alert [type]="colors.warning" class="d-block mb-3 spark-shared-notice">{{ text }}</bs-alert>
152
+ }
153
+ @switch (state()) {
154
+ @case ('loading') {
155
+ <bs-spinner class="spark-shared-loading" />
156
+ }
157
+ @case ('notFound') {
158
+ <bs-alert [type]="colors.danger" class="d-block spark-shared-not-found">{{ t('notFound') }}</bs-alert>
159
+ }
160
+ @case ('ready') {
161
+ <h1 class="h4 mb-3 spark-shared-title">{{ shared()?.label ?? undefined | resolveTranslation:(shared()?.type ?? '') }}</h1>
162
+ <dl class="row spark-shared-attributes">
163
+ @for (cell of attributeCells(); track cell.attribute.name) {
164
+ <dt class="col-sm-4">{{ cell.attribute.label | resolveTranslation:cell.attribute.name }}</dt>
165
+ <dd class="col-sm-8" [attr.data-attribute]="cell.attribute.name">
166
+ <spark-grid-cell [column]="cell.attribute" [display]="cell.display" [rendererValue]="cell.value" [item]="object()" />
167
+ </dd>
168
+ }
169
+ </dl>
170
+ @for (query of queryTables(); track query.name) {
171
+ <section class="mt-4 spark-shared-query" [attr.data-query]="query.name">
172
+ <h2 class="h5">{{ query.label | resolveTranslation:query.name }}</h2>
173
+ <div class="table-responsive">
174
+ <table class="table table-sm align-middle">
175
+ <thead>
176
+ <tr>
177
+ @for (column of query.columns; track column.name) {
178
+ <th scope="col">{{ column.label | resolveTranslation:column.name }}</th>
179
+ }
180
+ </tr>
181
+ </thead>
182
+ <tbody>
183
+ @for (item of query.result.items; track item.id) {
184
+ <tr [attr.data-id]="item.id">
185
+ @for (column of query.columns; track column.name) {
186
+ <td><spark-grid-cell [column]="column" [display]="display(column, item)" [rendererValue]="raw(item, column)" [item]="item" /></td>
187
+ }
188
+ </tr>
189
+ }
190
+ </tbody>
191
+ </table>
192
+ </div>
193
+ @if (query.result.totalItems > query.result.take) {
194
+ <nav class="d-flex align-items-center gap-2 spark-shared-pager">
195
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-shared-previous" aria-label="Previous"
196
+ [disabled]="query.result.skip === 0 || busy()" (click)="page(query.name, query.result.skip - query.result.take)">&lsaquo;</button>
197
+ <span class="small spark-shared-range">{{ query.result.skip + 1 }}–{{ query.last }} / {{ query.result.totalItems }}</span>
198
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-shared-next" aria-label="Next"
199
+ [disabled]="query.last >= query.result.totalItems || busy()" (click)="page(query.name, query.result.skip + query.result.take)">&rsaquo;</button>
200
+ </nav>
201
+ }
202
+ </section>
203
+ }
204
+ }
205
+ }
206
+ </div>
207
+ `,
208
+ isInline: true,
209
+ styles: [".spark-shared-object{max-width:72rem;margin:0 auto;padding:1.5rem 1rem}.spark-shared-attributes{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:.5rem 1.5rem;margin:0}.spark-shared-attributes dt{font-weight:600}.spark-shared-attributes dd{margin:0;min-width:0}@media(max-width:575.98px){.spark-shared-attributes{grid-template-columns:minmax(0,1fr);gap:.15rem}.spark-shared-attributes dd{margin-bottom:.6rem}}.spark-shared-query{overflow-x:auto}.spark-shared-query table{width:100%;border-collapse:collapse}.spark-shared-query th,.spark-shared-query td{padding:.35rem .5rem;border-bottom:1px solid var(--bs-border-color);text-align:start}\n"],
210
+ dependencies: [
211
+ {
212
+ kind: "component",
213
+ type: BsAlertComponent,
214
+ selector: "bs-alert",
215
+ inputs: [
216
+ "type",
217
+ "announce",
218
+ "isVisible"
219
+ ],
220
+ outputs: ["isVisibleChange", "afterOpenedOrClosed"]
221
+ },
222
+ {
223
+ kind: "component",
224
+ type: BsSpinnerComponent,
225
+ selector: "bs-spinner",
226
+ inputs: ["type", "color"]
227
+ },
228
+ {
229
+ kind: "component",
230
+ type: SparkGridCellComponent,
231
+ selector: "spark-grid-cell",
232
+ inputs: [
233
+ "column",
234
+ "display",
235
+ "rendererValue",
236
+ "item",
237
+ "chips",
238
+ "link"
239
+ ]
240
+ },
241
+ {
242
+ kind: "pipe",
243
+ type: ResolveTranslationPipe,
244
+ name: "resolveTranslation"
245
+ }
246
+ ],
247
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
248
+ });
249
+ };
250
+ i0.ɵɵngDeclareClassMetadata({
251
+ minVersion: "12.0.0",
252
+ version: "22.2.2",
253
+ ngImport: i0,
254
+ type: SparkSharedObjectComponent,
255
+ decorators: [{
256
+ type: Component,
257
+ args: [{
258
+ selector: "spark-shared-object",
259
+ standalone: true,
260
+ changeDetection: ChangeDetectionStrategy.OnPush,
261
+ imports: [
262
+ BsAlertComponent,
263
+ BsSpinnerComponent,
264
+ SparkGridCellComponent,
265
+ ResolveTranslationPipe
266
+ ],
267
+ template: `
268
+ <div class="spark-shared-object">
269
+ @if (notice(); as text) {
270
+ <bs-alert [type]="colors.warning" class="d-block mb-3 spark-shared-notice">{{ text }}</bs-alert>
271
+ }
272
+ @switch (state()) {
273
+ @case ('loading') {
274
+ <bs-spinner class="spark-shared-loading" />
275
+ }
276
+ @case ('notFound') {
277
+ <bs-alert [type]="colors.danger" class="d-block spark-shared-not-found">{{ t('notFound') }}</bs-alert>
278
+ }
279
+ @case ('ready') {
280
+ <h1 class="h4 mb-3 spark-shared-title">{{ shared()?.label ?? undefined | resolveTranslation:(shared()?.type ?? '') }}</h1>
281
+ <dl class="row spark-shared-attributes">
282
+ @for (cell of attributeCells(); track cell.attribute.name) {
283
+ <dt class="col-sm-4">{{ cell.attribute.label | resolveTranslation:cell.attribute.name }}</dt>
284
+ <dd class="col-sm-8" [attr.data-attribute]="cell.attribute.name">
285
+ <spark-grid-cell [column]="cell.attribute" [display]="cell.display" [rendererValue]="cell.value" [item]="object()" />
286
+ </dd>
287
+ }
288
+ </dl>
289
+ @for (query of queryTables(); track query.name) {
290
+ <section class="mt-4 spark-shared-query" [attr.data-query]="query.name">
291
+ <h2 class="h5">{{ query.label | resolveTranslation:query.name }}</h2>
292
+ <div class="table-responsive">
293
+ <table class="table table-sm align-middle">
294
+ <thead>
295
+ <tr>
296
+ @for (column of query.columns; track column.name) {
297
+ <th scope="col">{{ column.label | resolveTranslation:column.name }}</th>
298
+ }
299
+ </tr>
300
+ </thead>
301
+ <tbody>
302
+ @for (item of query.result.items; track item.id) {
303
+ <tr [attr.data-id]="item.id">
304
+ @for (column of query.columns; track column.name) {
305
+ <td><spark-grid-cell [column]="column" [display]="display(column, item)" [rendererValue]="raw(item, column)" [item]="item" /></td>
306
+ }
307
+ </tr>
308
+ }
309
+ </tbody>
310
+ </table>
311
+ </div>
312
+ @if (query.result.totalItems > query.result.take) {
313
+ <nav class="d-flex align-items-center gap-2 spark-shared-pager">
314
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-shared-previous" aria-label="Previous"
315
+ [disabled]="query.result.skip === 0 || busy()" (click)="page(query.name, query.result.skip - query.result.take)">&lsaquo;</button>
316
+ <span class="small spark-shared-range">{{ query.result.skip + 1 }}–{{ query.last }} / {{ query.result.totalItems }}</span>
317
+ <button type="button" class="btn btn-sm btn-outline-secondary spark-shared-next" aria-label="Next"
318
+ [disabled]="query.last >= query.result.totalItems || busy()" (click)="page(query.name, query.result.skip + query.result.take)">&rsaquo;</button>
319
+ </nav>
320
+ }
321
+ </section>
322
+ }
323
+ }
324
+ }
325
+ </div>
326
+ `,
327
+ styles: [".spark-shared-object{max-width:72rem;margin:0 auto;padding:1.5rem 1rem}.spark-shared-attributes{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:.5rem 1.5rem;margin:0}.spark-shared-attributes dt{font-weight:600}.spark-shared-attributes dd{margin:0;min-width:0}@media(max-width:575.98px){.spark-shared-attributes{grid-template-columns:minmax(0,1fr);gap:.15rem}.spark-shared-attributes dd{margin-bottom:.6rem}}.spark-shared-query{overflow-x:auto}.spark-shared-query table{width:100%;border-collapse:collapse}.spark-shared-query th,.spark-shared-query td{padding:.35rem .5rem;border-bottom:1px solid var(--bs-border-color);text-align:start}\n"]
328
+ }]
329
+ }],
330
+ ctorParameters: () => []
331
+ });
332
+ export { SparkSharedObjectComponent };
333
+
334
+ //# sourceMappingURL=mintplayer-ng-spark-share-page.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mintplayer-ng-spark-share-page.mjs","names":[],"sources":["../../share/page/src/spark-shared-object.component.ts"],"sourcesContent":["import { HttpClient, HttpErrorResponse } from '@angular/common/http';\nimport { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { firstValueFrom } from 'rxjs';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { SPARK_CONFIG } from '@mintplayer/ng-spark';\nimport { SparkGridCellComponent } from '@mintplayer/ng-spark/grid';\nimport { PersistentObject, PersistentObjectAttribute, QueryColumn, QueryResult, QueryResultItem, TranslatedString, valueFor } from '@mintplayer/ng-spark/models';\nimport { QueryCellValuePipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport { cellValue, rendererValue } from '@mintplayer/ng-spark/renderers';\nimport { SparkLanguageService } from '@mintplayer/ng-spark/services';\n\n/** What `POST /spark/share/load` answers: the declared attributes and the first page of each declared child query. */\nexport interface SparkSharedObject {\n id: string;\n type: string;\n /** The type's label (the model's, translated); the page's heading. */\n label?: TranslatedString | null;\n attributes: PersistentObjectAttribute[];\n /** By query name. */\n queries: Record<string, QueryResult>;\n /** Each shown query's label, by query name; the tables' headings. */\n queryLabels?: Record<string, TranslatedString | null>;\n}\n\n/** English fallbacks, shown until (or where) the server's translations have none. */\nconst TEXTS: Record<string, string> = {\n notFound: 'This link does not work (any more).',\n rateLimited: 'Too many requests. Try again in a minute.',\n unavailable: 'This page could not be loaded. Try again later.',\n};\n\ntype State = 'loading' | 'ready' | 'notFound' | 'failed';\n\n/**\n * The read-only page of a share link (`docs/seo_analyzer_PRD.md` X6, D31), routed by `withShareRoutes()`:\n * exactly what `POST /spark/share/load` returned, the object's attributes through the same cells (and\n * custom renderers, such as the score renderers) as the rest of Spark, and one read-only table per\n * child query, paged through `POST /spark/share/query`. Every refusal reads as the one \"this link does\n * not work\" (`share.notFound`), since the server answers every refusal with the same 404. No signed-in\n * user, no links into the app, no assumptions about the surrounding chrome.\n */\n@Component({\n selector: 'spark-shared-object',\n standalone: true,\n // Bootstrap's grid and table CSS are not global in ng-bootstrap (they ship with bs-grid / bs-table), so the\n // `row`/`col-sm-4`/`table` classes alone left the page unstyled and edge to edge (seen in SeoTester, M11).\n styles: [`\n .spark-shared-object { max-width: 72rem; margin: 0 auto; padding: 1.5rem 1rem; }\n .spark-shared-attributes { display: grid; grid-template-columns: minmax(8rem, max-content) minmax(0, 1fr); gap: .5rem 1.5rem; margin: 0; }\n .spark-shared-attributes dt { font-weight: 600; }\n .spark-shared-attributes dd { margin: 0; min-width: 0; }\n /* A phone stacks each label over its value: beside it, the label column took 40 % of 375 px. */\n @media (max-width: 575.98px) {\n .spark-shared-attributes { grid-template-columns: minmax(0, 1fr); gap: .15rem; }\n .spark-shared-attributes dd { margin-bottom: .6rem; }\n }\n .spark-shared-query { overflow-x: auto; }\n .spark-shared-query table { width: 100%; border-collapse: collapse; }\n .spark-shared-query th, .spark-shared-query td { padding: .35rem .5rem; border-bottom: 1px solid var(--bs-border-color); text-align: start; }\n `],\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [BsAlertComponent, BsSpinnerComponent, SparkGridCellComponent, ResolveTranslationPipe],\n template: `\n <div class=\"spark-shared-object\">\n @if (notice(); as text) {\n <bs-alert [type]=\"colors.warning\" class=\"d-block mb-3 spark-shared-notice\">{{ text }}</bs-alert>\n }\n @switch (state()) {\n @case ('loading') {\n <bs-spinner class=\"spark-shared-loading\" />\n }\n @case ('notFound') {\n <bs-alert [type]=\"colors.danger\" class=\"d-block spark-shared-not-found\">{{ t('notFound') }}</bs-alert>\n }\n @case ('ready') {\n <h1 class=\"h4 mb-3 spark-shared-title\">{{ shared()?.label ?? undefined | resolveTranslation:(shared()?.type ?? '') }}</h1>\n <dl class=\"row spark-shared-attributes\">\n @for (cell of attributeCells(); track cell.attribute.name) {\n <dt class=\"col-sm-4\">{{ cell.attribute.label | resolveTranslation:cell.attribute.name }}</dt>\n <dd class=\"col-sm-8\" [attr.data-attribute]=\"cell.attribute.name\">\n <spark-grid-cell [column]=\"cell.attribute\" [display]=\"cell.display\" [rendererValue]=\"cell.value\" [item]=\"object()\" />\n </dd>\n }\n </dl>\n @for (query of queryTables(); track query.name) {\n <section class=\"mt-4 spark-shared-query\" [attr.data-query]=\"query.name\">\n <h2 class=\"h5\">{{ query.label | resolveTranslation:query.name }}</h2>\n <div class=\"table-responsive\">\n <table class=\"table table-sm align-middle\">\n <thead>\n <tr>\n @for (column of query.columns; track column.name) {\n <th scope=\"col\">{{ column.label | resolveTranslation:column.name }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (item of query.result.items; track item.id) {\n <tr [attr.data-id]=\"item.id\">\n @for (column of query.columns; track column.name) {\n <td><spark-grid-cell [column]=\"column\" [display]=\"display(column, item)\" [rendererValue]=\"raw(item, column)\" [item]=\"item\" /></td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n @if (query.result.totalItems > query.result.take) {\n <nav class=\"d-flex align-items-center gap-2 spark-shared-pager\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-shared-previous\" aria-label=\"Previous\"\n [disabled]=\"query.result.skip === 0 || busy()\" (click)=\"page(query.name, query.result.skip - query.result.take)\">&lsaquo;</button>\n <span class=\"small spark-shared-range\">{{ query.result.skip + 1 }}–{{ query.last }} / {{ query.result.totalItems }}</span>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary spark-shared-next\" aria-label=\"Next\"\n [disabled]=\"query.last >= query.result.totalItems || busy()\" (click)=\"page(query.name, query.result.skip + query.result.take)\">&rsaquo;</button>\n </nav>\n }\n </section>\n }\n }\n }\n </div>\n `,\n})\nexport class SparkSharedObjectComponent {\n private readonly http = inject(HttpClient);\n private readonly route = inject(ActivatedRoute);\n private readonly language = inject(SparkLanguageService);\n private readonly baseUrl = `${inject(SPARK_CONFIG, { optional: true })?.baseUrl ?? '/spark'}/share`;\n private readonly cellPipe = new QueryCellValuePipe();\n protected readonly colors = Color;\n\n private readonly type = this.route.snapshot.paramMap.get('type') ?? '';\n private readonly token = this.route.snapshot.paramMap.get('token') ?? '';\n private readonly id = this.route.snapshot.queryParamMap.get('id') ?? '';\n\n readonly state = signal<State>('loading');\n readonly shared = signal<SparkSharedObject | null>(null);\n /** The page shown of each child query, by name: the first from the load, then whatever was paged to. */\n readonly pages = signal<Record<string, QueryResult>>({});\n readonly busy = signal(false);\n /** A passing problem (rate limit, network) shown above the content, which stays. */\n readonly notice = signal<string | null>(null);\n\n /** The object in the shape renderers receive as `item`. */\n protected readonly object = computed(() => {\n const shared = this.shared();\n return shared ? ({ id: shared.id, name: shared.type, objectTypeId: shared.type, attributes: shared.attributes } as PersistentObject) : null;\n });\n\n protected readonly attributeCells = computed(() => {\n const object = this.object();\n if (!object) return [];\n return [...object.attributes]\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))\n .map(attribute => ({\n attribute,\n display: this.cellPipe.transform(attribute as unknown as QueryColumn, object as unknown as QueryResultItem, {}),\n // As the detail page: an AsDetail value travels in `object`/`objects`, not `value`.\n value: rendererValue(attribute),\n }));\n });\n\n protected readonly queryTables = computed(() =>\n Object.entries(this.pages()).map(([name, result]) => ({\n name,\n label: this.shared()?.queryLabels?.[name] ?? undefined,\n result,\n columns: [...(result.columns ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),\n last: Math.min(result.skip + result.take, result.totalItems),\n })));\n\n constructor() {\n void this.load();\n }\n\n protected t(key: string): string {\n return this.language.t('share.' + key, TEXTS[key]);\n }\n\n protected display(column: QueryColumn, item: QueryResultItem): unknown {\n return this.cellPipe.transform(column, item, {});\n }\n\n protected raw(item: QueryResultItem, column: QueryColumn): unknown {\n return cellValue(valueFor(item, column.name));\n }\n\n async load(): Promise<void> {\n if (!this.type || !this.token || !this.id) {\n this.state.set('notFound');\n return;\n }\n this.state.set('loading');\n try {\n const shared = await this.post<SparkSharedObject>('load', {});\n this.shared.set({ ...shared, attributes: shared.attributes ?? [], queries: shared.queries ?? {} });\n this.pages.set({ ...(shared.queries ?? {}) });\n this.notice.set(null);\n this.state.set('ready');\n } catch (error) {\n this.fail(error);\n }\n }\n\n /** Shows the page of `query` that starts at `skip`, the same size as the one shown. */\n async page(query: string, skip: number): Promise<void> {\n const current = this.pages()[query];\n if (!current || skip < 0 || this.busy()) return;\n this.busy.set(true);\n try {\n const result = await this.post<QueryResult>('query', { query, skip, take: current.take });\n this.pages.update(pages => ({ ...pages, [query]: result }));\n this.notice.set(null);\n } catch (error) {\n this.fail(error);\n } finally {\n this.busy.set(false);\n }\n }\n\n private post<T>(path: 'load' | 'query', extra: Record<string, unknown>): Promise<T> {\n return firstValueFrom(this.http.post<T>(`${this.baseUrl}/${path}`, { type: this.type, id: this.id, token: this.token, ...extra }));\n }\n\n private fail(error: unknown): void {\n const status = error instanceof HttpErrorResponse ? error.status : 0;\n if (status === 404) {\n // Revoked, replaced, switched off or never valid: the server does not say which, nor does the page.\n this.shared.set(null);\n this.pages.set({});\n this.notice.set(null);\n this.state.set('notFound');\n return;\n }\n this.notice.set(this.t(status === 429 ? 'rateLimited' : 'unavailable'));\n if (this.state() === 'loading') this.state.set('failed');\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ function withShareRoutes(options) {
2
+ const component = options?.component;
3
+ return [{
4
+ path: options?.path ?? "shared/:type/:token",
5
+ loadComponent: component ? () => Promise.resolve(component) : () => import("@mintplayer/ng-spark/share/page").then((m) => m.SparkSharedObjectComponent)
6
+ }];
7
+ }
8
+ export { withShareRoutes };
9
+
10
+ //# sourceMappingURL=mintplayer-ng-spark-share.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mintplayer-ng-spark-share.mjs","names":[],"sources":["../../share/src/share-routes.ts"],"sourcesContent":["import type { Type } from '@angular/core';\nimport type { Routes } from '@angular/router';\n\n// Type-only: the page stays out of this entry point's bundle, loaded on navigation.\nexport type { SparkSharedObject } from '@mintplayer/ng-spark/share/page';\n\n/** Options of {@link withShareRoutes}. */\nexport interface SparkShareRoutesOptions {\n /** The route path; it must hold `:type` and `:token`. Default `shared/:type/:token`, matching the server's `Spark:Share:LinkPath` (`/shared`). */\n path?: string;\n /** A page to use instead of `SparkSharedObjectComponent`. */\n component?: Type<unknown>;\n}\n\n/**\n * The page a share link opens (`docs/seo_analyzer_PRD.md` X6, D31): `{origin}/shared/{type}/{token}?id={id}`.\n * Anonymous by design (the token is the credential), so it carries no guard; put it where no app chrome\n * assumes a signed-in user:\n *\n * ```ts\n * export const routes: Routes = [\n * ...withShareRoutes(),\n * { path: '', component: ShellComponent, children: [...sparkRoutes()] },\n * ];\n * ```\n *\n * Moving the page means setting `Spark:Share:LinkPath` to match, so the links the server makes follow.\n */\nexport function withShareRoutes(options?: SparkShareRoutesOptions): Routes {\n const component = options?.component;\n return [\n {\n path: options?.path ?? 'shared/:type/:token',\n loadComponent: component\n ? () => Promise.resolve(component)\n : () => import('@mintplayer/ng-spark/share/page').then(m => m.SparkSharedObjectComponent),\n },\n ];\n}\n"],"mappings":"AA4BA,SAAgB,gBAAgB,SAAiC;CAC/D,MAAM,YAAY,SAAS;CAC3B,OAAO,CACL;EACE,MAAM,SAAS,QAAQ;EACvB,eAAe,kBACL,QAAQ,QAAQ,SAAS,UACzB,OAAO,kCAAkC,CAAC,MAAK,MAAK,EAAE,0BAA0B;CAC3F,CACF;AACH"}