@mintplayer/ng-spark 22.23.0 → 22.25.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 (71) hide show
  1. package/README.md +196 -0
  2. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +100 -47
  3. package/fesm2022/mintplayer-ng-spark-attribute-description.mjs.map +1 -1
  4. package/fesm2022/mintplayer-ng-spark-client-operations.mjs +310 -329
  5. package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
  6. package/fesm2022/mintplayer-ng-spark-column-filter.mjs +175 -135
  7. package/fesm2022/mintplayer-ng-spark-column-filter.mjs.map +1 -1
  8. package/fesm2022/mintplayer-ng-spark-contributions.mjs +721 -0
  9. package/fesm2022/mintplayer-ng-spark-contributions.mjs.map +1 -0
  10. package/fesm2022/mintplayer-ng-spark-grid.mjs +2385 -1118
  11. package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
  12. package/fesm2022/mintplayer-ng-spark-history.mjs +598 -0
  13. package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -0
  14. package/fesm2022/mintplayer-ng-spark-icon.mjs +64 -27
  15. package/fesm2022/mintplayer-ng-spark-icon.mjs.map +1 -1
  16. package/fesm2022/mintplayer-ng-spark-models.mjs +405 -762
  17. package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
  18. package/fesm2022/mintplayer-ng-spark-moderation.mjs +1212 -0
  19. package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -0
  20. package/fesm2022/mintplayer-ng-spark-panels.mjs +60 -0
  21. package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -0
  22. package/fesm2022/mintplayer-ng-spark-pipes.mjs +1158 -765
  23. package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
  24. package/fesm2022/mintplayer-ng-spark-po-create.mjs +273 -169
  25. package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
  26. package/fesm2022/mintplayer-ng-spark-po-detail.mjs +795 -410
  27. package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
  28. package/fesm2022/mintplayer-ng-spark-po-edit.mjs +1196 -280
  29. package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
  30. package/fesm2022/mintplayer-ng-spark-po-form.mjs +2244 -1224
  31. package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
  32. package/fesm2022/mintplayer-ng-spark-query-list.mjs +571 -303
  33. package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
  34. package/fesm2022/mintplayer-ng-spark-renderers.mjs +44 -55
  35. package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
  36. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +216 -177
  37. package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
  38. package/fesm2022/mintplayer-ng-spark-routes.mjs +24 -28
  39. package/fesm2022/mintplayer-ng-spark-routes.mjs.map +1 -1
  40. package/fesm2022/mintplayer-ng-spark-services.mjs +625 -640
  41. package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
  42. package/fesm2022/mintplayer-ng-spark-shell.mjs +1031 -397
  43. package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
  44. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +294 -0
  45. package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -0
  46. package/fesm2022/mintplayer-ng-spark.mjs +14 -48
  47. package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
  48. package/package.json +23 -2
  49. package/types/mintplayer-ng-spark-attribute-description.d.ts +25 -18
  50. package/types/mintplayer-ng-spark-client-operations.d.ts +139 -160
  51. package/types/mintplayer-ng-spark-column-filter.d.ts +73 -55
  52. package/types/mintplayer-ng-spark-contributions.d.ts +231 -0
  53. package/types/mintplayer-ng-spark-grid.d.ts +917 -448
  54. package/types/mintplayer-ng-spark-history.d.ts +216 -0
  55. package/types/mintplayer-ng-spark-icon.d.ts +15 -13
  56. package/types/mintplayer-ng-spark-models.d.ts +681 -573
  57. package/types/mintplayer-ng-spark-moderation.d.ts +402 -0
  58. package/types/mintplayer-ng-spark-panels.d.ts +143 -0
  59. package/types/mintplayer-ng-spark-pipes.d.ts +131 -149
  60. package/types/mintplayer-ng-spark-po-create.d.ts +46 -37
  61. package/types/mintplayer-ng-spark-po-detail.d.ts +180 -117
  62. package/types/mintplayer-ng-spark-po-edit.d.ts +367 -64
  63. package/types/mintplayer-ng-spark-po-form.d.ts +558 -373
  64. package/types/mintplayer-ng-spark-query-list.d.ts +143 -82
  65. package/types/mintplayer-ng-spark-renderers.d.ts +121 -85
  66. package/types/mintplayer-ng-spark-retry-action-modal.d.ts +31 -34
  67. package/types/mintplayer-ng-spark-routes.d.ts +7 -11
  68. package/types/mintplayer-ng-spark-services.d.ts +377 -261
  69. package/types/mintplayer-ng-spark-shell.d.ts +228 -132
  70. package/types/mintplayer-ng-spark-soft-delete.d.ts +90 -0
  71. package/types/mintplayer-ng-spark.d.ts +19 -12
@@ -1,304 +1,572 @@
1
- import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, input, output, signal, viewChild, computed, effect, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
- import * as i1 from '@angular/common';
5
- import { CommonModule, NgTemplateOutlet } from '@angular/common';
6
- import * as i2 from '@angular/forms';
7
- import { FormsModule } from '@angular/forms';
8
- import { ActivatedRoute, Router } from '@angular/router';
9
- import { Color } from '@mintplayer/ng-bootstrap';
10
- import { BsBadgeComponent } from '@mintplayer/ng-bootstrap/badge';
11
- import { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';
12
- import { BsFormComponent, BsFormControlDirective } from '@mintplayer/ng-bootstrap/form';
13
- import { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';
14
- import { BsInputGroupComponent } from '@mintplayer/ng-bootstrap/input-group';
15
- import { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';
16
- import { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';
17
- import { SparkService, SparkStreamingService, SparkLanguageService } from '@mintplayer/ng-spark/services';
18
- import { ResolveTranslationPipe, TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';
19
- import { SparkIconComponent } from '@mintplayer/ng-spark/icon';
20
- import { SparkQueryGridComponent } from '@mintplayer/ng-spark/grid';
21
-
22
- /**
23
- * The routed query page: chrome around one {@link SparkQueryGridComponent}.
24
- *
25
- * It owns what is genuinely page-shaped and route-shaped, and nothing else:
26
- *
27
- * - **Route resolution.** It has no `queryId` input; it reads `paramMap`, and it serves two
28
- * routes — `query/:queryId`, and `po/:type`, which resolves an entity type to a query. That
29
- * second one is type-to-query resolution, not query rendering, and is why this component still
30
- * exists rather than the router pointing at the grid.
31
- * - **Streaming.** The websocket lives here so it stays out of every PO detail page's bundle;
32
- * the snapshot is filtered and sorted client-side and handed to the grid as `[data]`.
33
- * - The action bar, the caption, the LIVE badge, the search box and the New button.
34
- *
35
- * The grid itself — columns, cells, paging, the row link, selection, custom-action execution —
36
- * is the shared component. This page previously wrote out `<bs-datatable>` twice, once per
37
- * transport, with a shared row template between them; both are gone.
38
- */
39
- class SparkQueryListComponent {
40
- route = inject(ActivatedRoute);
41
- router = inject(Router);
42
- sparkService = inject(SparkService);
43
- streamingService = inject(SparkStreamingService);
44
- destroyRef = inject(DestroyRef);
45
- lang = inject(SparkLanguageService);
46
- extraActionsTemplate = input(null, /* @ts-ignore */
47
- ...(ngDevMode ? [{ debugName: "extraActionsTemplate" }] : /* istanbul ignore next */ []));
48
- showCustomActions = input(true, /* @ts-ignore */
49
- ...(ngDevMode ? [{ debugName: "showCustomActions" }] : /* istanbul ignore next */ []));
50
- /**
51
- * Forwarded to the grid, so a query PAGE can replace its row links — the same reason the card
52
- * forwards it. Without this the escape hatch existed only for a directly-embedded grid.
53
- */
54
- rowRoute = input(null, /* @ts-ignore */
55
- ...(ngDevMode ? [{ debugName: "rowRoute" }] : /* istanbul ignore next */ []));
56
- rowClicked = output();
57
- createClicked = output();
58
- customActionExecuted = output();
59
- colors = Color;
60
- /** The query the grid should render, resolved from the route. Null until it is known. */
61
- queryId = signal(null, /* @ts-ignore */
62
- ...(ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []));
63
- errorMessage = signal(null, /* @ts-ignore */
64
- ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
65
- searchTerm = signal('', /* @ts-ignore */
66
- ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
67
- grid = viewChild(SparkQueryGridComponent, /* @ts-ignore */
68
- ...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
69
- /**
70
- * Grid state, surfaced for this page's chrome.
71
- *
72
- * Optional `viewChild`, read defensively: the action bar and caption render above the grid, so
73
- * on the first change-detection pass the query has not resolved yet.
74
- */
75
- query = computed(() => this.grid()?.query() ?? null, /* @ts-ignore */
76
- ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
77
- entityType = computed(() => this.grid()?.entityType() ?? null, /* @ts-ignore */
78
- ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
79
- customActions = computed(() => this.grid()?.customActions() ?? [], /* @ts-ignore */
80
- ...(ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []));
81
- canCreate = computed(() => this.grid()?.canCreate() ?? false, /* @ts-ignore */
82
- ...(ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []));
83
- resultCount = computed(() => this.grid()?.resultCount() ?? null, /* @ts-ignore */
84
- ...(ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []));
85
- isVirtualScrolling = computed(() => this.grid()?.isVirtualScrolling() ?? false, /* @ts-ignore */
86
- ...(ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []));
87
- gridError = computed(() => this.grid()?.errorMessage() ?? null, /* @ts-ignore */
88
- ...(ngDevMode ? [{ debugName: "gridError" }] : /* istanbul ignore next */ []));
89
- /** Whether an action's selection rule is satisfied. Delegated: the grid holds the selection. */
90
- /** @see SparkPoDetailComponent.customActionClass — same allow-list, same default. */
91
- customActionClass(action) {
92
- const variant = action.variant?.toLowerCase();
93
- switch (variant) {
94
- case 'danger':
95
- case 'warning':
96
- case 'primary':
97
- case 'secondary':
98
- case 'success':
99
- return `btn btn-${variant}`;
100
- default:
101
- return 'btn btn-outline-primary';
102
- }
103
- }
104
- isActionEnabled(action) {
105
- return this.grid()?.isActionEnabled(action) ?? false;
106
- }
107
- // --- streaming ---------------------------------------------------------------
108
- isStreaming = signal(false, /* @ts-ignore */
109
- ...(ngDevMode ? [{ debugName: "isStreaming" }] : /* istanbul ignore next */ []));
110
- streamingSub = null;
111
- /** Columns as sent with the stream's snapshot; empty until it arrives. */
112
- streamColumns = signal([], /* @ts-ignore */
113
- ...(ngDevMode ? [{ debugName: "streamColumns" }] : /* istanbul ignore next */ []));
114
- allItems = signal([], /* @ts-ignore */
115
- ...(ngDevMode ? [{ debugName: "allItems" }] : /* istanbul ignore next */ []));
116
- streamItems = signal([], /* @ts-ignore */
117
- ...(ngDevMode ? [{ debugName: "streamItems" }] : /* istanbul ignore next */ []));
118
- /**
119
- * Rows handed to the grid, or `null` to let it fetch for itself.
120
- *
121
- * Null for a normal query — an empty array would read as "here are no rows" and suppress the
122
- * fetch entirely.
123
- */
124
- gridData = computed(() => this.query()?.isStreamingQuery ? this.streamItems() : null, /* @ts-ignore */
125
- ...(ngDevMode ? [{ debugName: "gridData" }] : /* istanbul ignore next */ []));
126
- constructor() {
127
- this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => {
128
- // The handler is async and this is a subscribe, so a rejection lands nowhere: the metadata
129
- // load would reject, the query would stay null, and the template would render a spinner
130
- // FOREVER — while this component has had an errorMessage surface all along that only the
131
- // fetch path ever reached.
132
- this.onParamsChange(params).catch((e) => this.reportLoadFailure(e));
133
- });
134
- this.destroyRef.onDestroy(() => this.disconnectStreaming());
135
- // Connect the socket once the grid has resolved a streaming query, and disconnect whenever it
136
- // resolves anything else. The grid knows not to fetch for a streaming query, so there is no
137
- // window in which both transports are live.
138
- effect(() => {
139
- const q = this.query();
140
- if (q?.isStreamingQuery) {
141
- this.connectStreaming(q.id);
142
- }
143
- else {
144
- this.disconnectStreaming();
145
- }
146
- });
147
- // Client-side filter and sort for the streaming snapshot. The server never sees these: there
148
- // is no request to attach them to.
149
- effect(() => {
150
- this.searchTerm();
151
- this.grid()?.settings();
152
- this.allItems();
153
- if (this.isStreaming())
154
- this.applyFilter();
155
- });
156
- }
157
- async onParamsChange(params) {
158
- this.errorMessage.set(null);
159
- this.queryId.set(null);
160
- this.allItems.set([]);
161
- this.streamItems.set([]);
162
- this.streamColumns.set([]);
163
- this.disconnectStreaming();
164
- const queryId = params.get('queryId');
165
- const typeParam = params.get('type');
166
- if (queryId) {
167
- this.queryId.set(queryId);
168
- return;
169
- }
170
- if (!typeParam)
171
- return;
172
- // `po/:type` — the route names an entity type, so find the query that lists it. The grid takes
173
- // a query, and this translation is the only reason it cannot take the route directly.
174
- const [entityTypes, queries] = await Promise.all([
175
- this.sparkService.getEntityTypes(),
176
- this.sparkService.getQueries(),
177
- ]);
178
- const entityType = entityTypes.find(t => t.id === typeParam || t.alias === typeParam);
179
- if (!entityType) {
180
- this.reportLoadFailure({ status: 404 });
181
- return;
182
- }
183
- const singularName = entityType.name;
184
- const match = queries.find(q => {
185
- if (q.entityType === singularName)
186
- return true;
187
- const sourceName = q.source.includes('.') ? q.source.substring(q.source.indexOf('.') + 1) : q.source;
188
- return sourceName === singularName || sourceName === singularName + 's';
189
- });
190
- if (match)
191
- this.queryId.set(match.alias || match.id);
192
- else
193
- this.reportLoadFailure({ status: 404 });
194
- }
195
- /**
196
- * A load failure has to render, not just be swallowed: a denied query answers 404 (audit M-3, so
197
- * existence is not leaked), which is indistinguishable from a missing one — hence a deliberately
198
- * generic message rather than a guess at which it was.
199
- */
200
- reportLoadFailure(err) {
201
- this.errorMessage.set(err?.status === 404
202
- ? (this.lang.t('spark.query.unavailable') || 'This list is not available.')
203
- : (err?.error?.error || err?.message || 'An unexpected error occurred'));
204
- }
205
- async onCustomAction(action) {
206
- await this.grid()?.onCustomAction(action);
207
- }
208
- onCreate() {
209
- this.createClicked.emit();
210
- const et = this.entityType();
211
- if (et)
212
- this.router.navigate(['/po', et.alias || et.id, 'new']);
213
- }
214
- clearSearch() {
215
- this.searchTerm.set('');
216
- }
217
- connectStreaming(queryId) {
218
- if (this.streamingSub)
219
- return;
220
- this.isStreaming.set(true);
221
- this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({
222
- next: (message) => this.handleStreamingMessage(message),
223
- error: (err) => {
224
- this.errorMessage.set(err?.message || 'Streaming connection failed');
225
- this.isStreaming.set(false);
226
- },
227
- complete: () => this.isStreaming.set(false),
228
- });
229
- }
230
- disconnectStreaming() {
231
- if (this.streamingSub) {
232
- this.streamingSub.unsubscribe();
233
- this.streamingSub = null;
234
- }
235
- this.isStreaming.set(false);
236
- }
237
- handleStreamingMessage(message) {
238
- switch (message.type) {
239
- case 'snapshot':
240
- this.errorMessage.set(null);
241
- // Columns come with the snapshot and never change for the life of the stream, so they are
242
- // stored once and handed to the grid alongside the rows — a projection cannot describe
243
- // itself, and the grid has no entity-type metadata to fall back on any more.
244
- this.streamColumns.set(message.columns);
245
- this.allItems.set(message.data);
246
- break;
247
- case 'patch':
248
- if (message.updated.length > 0) {
249
- this.allItems.update(items => items.map(item => {
250
- const patch = message.updated.find(u => u.id === item.id);
251
- if (!patch)
252
- return item;
253
- return {
254
- ...item,
255
- values: item.values.map(v => v.key in patch.values ? { ...v, value: patch.values[v.key] } : v),
256
- };
257
- }));
258
- }
259
- break;
260
- case 'error':
261
- this.errorMessage.set(message.message);
262
- break;
263
- }
264
- }
265
- applyFilter() {
266
- let items = this.allItems();
267
- const term = this.searchTerm().toLowerCase();
268
- if (term) {
269
- items = items.filter(item => item.values.some(v => String(v.value ?? '').toLowerCase().includes(term)));
270
- }
271
- // The datatable in `[data]` mode also sorts on header clicks, but the sort must survive a
272
- // patch: re-deriving from `allItems` without re-applying it would silently reorder the grid
273
- // under the user on every update.
274
- const sortCols = this.grid()?.settings().sortColumns ?? [];
275
- if (sortCols.length > 0) {
276
- items = [...items].sort((a, b) => {
277
- for (const col of sortCols) {
278
- const aVal = a.values.find(v => v.key === col.property)?.value ?? '';
279
- const bVal = b.values.find(v => v.key === col.property)?.value ?? '';
280
- const cmp = String(aVal).localeCompare(String(bVal));
281
- if (cmp !== 0)
282
- return col.direction === 'descending' ? -cmp : cmp;
283
- }
284
- return 0;
285
- });
286
- }
287
- this.streamItems.set(items);
288
- }
289
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
290
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkQueryListComponent, isStandalone: true, selector: "spark-query-list", inputs: { extraActionsTemplate: { classPropertyName: "extraActionsTemplate", publicName: "extraActionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, showCustomActions: { classPropertyName: "showCustomActions", publicName: "showCustomActions", isSignal: true, isRequired: false, transformFunction: null }, rowRoute: { classPropertyName: "rowRoute", publicName: "rowRoute", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowClicked: "rowClicked", createClicked: "createClicked", customActionExecuted: "customActionExecuted" }, host: { properties: { "class.virtual-scrolling": "isVirtualScrolling()" } }, viewQueries: [{ propertyName: "grid", first: true, predicate: SparkQueryGridComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @if (canCreate()) {\n <button *bsPriorityNavItem=\"1\" class=\"btn btn-primary\" (click)=\"onCreate()\">\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of customActions(); track action.name) {\n <!-- Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rule. -->\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\"\n [disabled]=\"!isActionEnabled(action)\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-form>\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\">\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </bs-form>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n", styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 calc(-1 * var(--spark-main-padding, 1.5rem)) calc(-1 * var(--spark-main-padding, 1.5rem));--spark-content-inset-inline: 0px}:host.virtual-scrolling spark-query-grid{display:flex;flex-direction:column;min-height:0}:host.virtual-scrolling ::ng-deep bs-datatable{display:flex;flex-direction:column;min-height:0;--mp-datatable-virtual-max-height: 100%}:host.virtual-scrolling ::ng-deep bs-datatable mp-datatable{flex:1 1 auto;min-height:0}tr:hover{background-color:#0000000d}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}.spark-live-badge{font-size:.6em}\n"], dependencies: [{ kind: "component", type: BsBadgeComponent, selector: "bs-badge", inputs: ["type", "unit", "decorative"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsFormComponent, selector: "bs-form", inputs: ["action", "method"], outputs: ["submitted"] }, { kind: "directive", type: BsFormControlDirective, selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "component", type: BsInputGroupComponent, selector: "bs-input-group", inputs: ["size"] }, { kind: "component", type: BsPriorityNavComponent, selector: "bs-priority-nav", inputs: ["moreLabel", "moreLabelTemplate", "collapseAt", "overflowFrom", "hideEmptyMore", "ariaLabel"], outputs: ["overflowChange"] }, { kind: "directive", type: BsPriorityNavItemDirective, selector: "[bsPriorityNavItem]", inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"] }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "component", type: SparkQueryGridComponent, selector: "spark-query-grid", inputs: ["queryId", "parentId", "parentType", "data", "search", "columns", "reloadToken", "settings", "selection", "rowRoute"], outputs: ["settingsChange", "selectionChange", "error", "rowClicked", "customActionExecuted"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
291
- }
292
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryListComponent, decorators: [{
293
- type: Component,
294
- args: [{ selector: 'spark-query-list', imports: [BsBadgeComponent, CommonModule, NgTemplateOutlet, FormsModule, BsAlertComponent, BsFormComponent, BsFormControlDirective, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsInputGroupComponent, BsPriorityNavComponent, BsPriorityNavItemDirective, BsSpinnerComponent, SparkIconComponent, SparkQueryGridComponent, ResolveTranslationPipe, TranslateKeyPipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
295
- '[class.virtual-scrolling]': 'isVirtualScrolling()'
296
- }, template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @if (canCreate()) {\n <button *bsPriorityNavItem=\"1\" class=\"btn btn-primary\" (click)=\"onCreate()\">\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n </button>\n }\n @if (showCustomActions()) {\n @for (action of customActions(); track action.name) {\n <!-- Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rule. -->\n <button *bsPriorityNavItem=\"10 + action.offset\" [class]=\"customActionClass(action)\"\n [disabled]=\"!isActionEnabled(action)\" (click)=\"onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-form>\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <bs-input-group>\n <span class=\"addon\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\">\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </bs-form>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n", styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 calc(-1 * var(--spark-main-padding, 1.5rem)) calc(-1 * var(--spark-main-padding, 1.5rem));--spark-content-inset-inline: 0px}:host.virtual-scrolling spark-query-grid{display:flex;flex-direction:column;min-height:0}:host.virtual-scrolling ::ng-deep bs-datatable{display:flex;flex-direction:column;min-height:0;--mp-datatable-virtual-max-height: 100%}:host.virtual-scrolling ::ng-deep bs-datatable mp-datatable{flex:1 1 auto;min-height:0}tr:hover{background-color:#0000000d}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}.spark-live-badge{font-size:.6em}\n"] }]
297
- }], ctorParameters: () => [], propDecorators: { extraActionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraActionsTemplate", required: false }] }], showCustomActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCustomActions", required: false }] }], rowRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowRoute", required: false }] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], createClicked: [{ type: i0.Output, args: ["createClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }], grid: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SparkQueryGridComponent), { isSignal: true }] }] } });
298
-
299
- /**
300
- * Generated bundle index. Do not edit.
301
- */
302
-
1
+ import * as i0 from "@angular/core";
2
+ import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, viewChild } from "@angular/core";
3
+ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
4
+ import * as i1 from "@angular/common";
5
+ import { CommonModule, NgComponentOutlet, NgTemplateOutlet } from "@angular/common";
6
+ import { ActivatedRoute, Router } from "@angular/router";
7
+ import { Color } from "@mintplayer/ng-bootstrap";
8
+ import { BsBadgeComponent } from "@mintplayer/ng-bootstrap/badge";
9
+ import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
10
+ import { BsGridColumnDirective, BsGridComponent, BsGridRowDirective } from "@mintplayer/ng-bootstrap/grid";
11
+ import { BsPriorityNavComponent, BsPriorityNavItemDirective } from "@mintplayer/ng-bootstrap/priority-nav";
12
+ import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
13
+ import { SparkLanguageService, SparkService, SparkStreamingService } from "@mintplayer/ng-spark/services";
14
+ import { ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
15
+ import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
16
+ import { SparkQueryGridComponent, SparkSearchBoxComponent, sparkActionClass } from "@mintplayer/ng-spark/grid";
17
+ import { SPARK_QUERY_LIST_ACTIONS, orderSparkExtensions, parseSparkDeletedParam } from "@mintplayer/ng-spark/panels";
18
+ var SparkQueryListComponent = class SparkQueryListComponent {
19
+ route = inject(ActivatedRoute);
20
+ router = inject(Router);
21
+ sparkService = inject(SparkService);
22
+ streamingService = inject(SparkStreamingService);
23
+ destroyRef = inject(DestroyRef);
24
+ lang = inject(SparkLanguageService);
25
+ extraActionsTemplate = input(null, ...ngDevMode ? [{ debugName: "extraActionsTemplate" }] : /* istanbul ignore next */ []);
26
+ showCustomActions = input(true, ...ngDevMode ? [{ debugName: "showCustomActions" }] : /* istanbul ignore next */ []);
27
+ rowRoute = input(null, ...ngDevMode ? [{ debugName: "rowRoute" }] : /* istanbul ignore next */ []);
28
+ rowClicked = output();
29
+ createClicked = output();
30
+ customActionExecuted = output();
31
+ colors = Color;
32
+ queryId = signal(null, ...ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []);
33
+ errorMessage = signal(null, ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
34
+ searchTerm = signal("", ...ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []);
35
+ grid = viewChild(SparkQueryGridComponent, ...ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []);
36
+ query = computed(() => this.grid()?.query() ?? null, ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
37
+ entityType = computed(() => this.grid()?.entityType() ?? null, ...ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []);
38
+ customActions = computed(() => this.grid()?.customActions() ?? [], ...ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []);
39
+ canCreate = computed(() => this.grid()?.offersCreate() ?? false, ...ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []);
40
+ resultCount = computed(() => this.grid()?.resultCount() ?? null, ...ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []);
41
+ isVirtualScrolling = computed(() => this.grid()?.isVirtualScrolling() ?? false, ...ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []);
42
+ gridError = computed(() => this.grid()?.errorMessage() ?? null, ...ngDevMode ? [{ debugName: "gridError" }] : /* istanbul ignore next */ []);
43
+ permissions = computed(() => this.grid()?.permissions() ?? null, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
44
+ listActions = orderSparkExtensions(inject(SPARK_QUERY_LIST_ACTIONS, { optional: true }), (a) => a.priority ?? 60);
45
+ deletedMode = signal("exclude", ...ngDevMode ? [{ debugName: "deletedMode" }] : /* istanbul ignore next */ []);
46
+ reloadToken = signal(0, ...ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []);
47
+ listContext = computed(() => {
48
+ const query = this.query();
49
+ if (!query) return null;
50
+ return {
51
+ query,
52
+ entityType: this.entityType(),
53
+ permissions: this.permissions(),
54
+ deleted: this.deletedMode(),
55
+ setDeleted: (mode) => this.setDeleted(mode),
56
+ reload: () => this.reloadToken.update((n) => n + 1)
57
+ };
58
+ }, ...ngDevMode ? [{ debugName: "listContext" }] : /* istanbul ignore next */ []);
59
+ gridReloadToken = this.reloadToken.asReadonly();
60
+ setDeleted(mode) {
61
+ this.router.navigate([], {
62
+ relativeTo: this.route,
63
+ queryParams: { deleted: mode === "exclude" ? null : mode },
64
+ queryParamsHandling: "merge",
65
+ replaceUrl: true
66
+ });
67
+ }
68
+ customActionClass(action) {
69
+ const variant = action.variant?.toLowerCase();
70
+ switch (variant) {
71
+ case "danger":
72
+ case "warning":
73
+ case "primary":
74
+ case "secondary":
75
+ case "success": return `btn btn-${variant}`;
76
+ default: return "btn btn-outline-primary";
77
+ }
78
+ }
79
+ toolbarActions = computed(() => this.grid()?.toolbarActions() ?? [], ...ngDevMode ? [{ debugName: "toolbarActions" }] : /* istanbul ignore next */ []);
80
+ toolbarActionClass(action) {
81
+ return action.kind === "new" ? "btn btn-primary" : sparkActionClass(action.definition);
82
+ }
83
+ isToolbarActionEnabled(action) {
84
+ return this.grid()?.isToolbarActionEnabled(action) ?? false;
85
+ }
86
+ runToolbarAction(action) {
87
+ if (action.kind === "new") {
88
+ this.onCreate();
89
+ return;
90
+ }
91
+ this.grid()?.runToolbarAction(action);
92
+ }
93
+ isActionEnabled(action) {
94
+ return this.grid()?.isActionEnabled(action) ?? false;
95
+ }
96
+ isStreaming = signal(false, ...ngDevMode ? [{ debugName: "isStreaming" }] : /* istanbul ignore next */ []);
97
+ streamingSub = null;
98
+ streamColumns = signal([], ...ngDevMode ? [{ debugName: "streamColumns" }] : /* istanbul ignore next */ []);
99
+ allItems = signal([], ...ngDevMode ? [{ debugName: "allItems" }] : /* istanbul ignore next */ []);
100
+ streamItems = signal([], ...ngDevMode ? [{ debugName: "streamItems" }] : /* istanbul ignore next */ []);
101
+ gridData = computed(() => this.query()?.isStreamingQuery ? this.streamItems() : null, ...ngDevMode ? [{ debugName: "gridData" }] : /* istanbul ignore next */ []);
102
+ constructor() {
103
+ this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => {
104
+ this.onParamsChange(params).catch((e) => this.reportLoadFailure(e));
105
+ });
106
+ this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((query) => {
107
+ this.deletedMode.set(parseSparkDeletedParam(query.get("deleted")) ?? "exclude");
108
+ this.applyUrlScope(query);
109
+ });
110
+ this.destroyRef.onDestroy(() => this.disconnectStreaming());
111
+ effect(() => {
112
+ const q = this.query();
113
+ if (q?.isStreamingQuery) this.connectStreaming(q.id);
114
+ else this.disconnectStreaming();
115
+ });
116
+ effect(() => {
117
+ this.searchTerm();
118
+ this.grid()?.settings();
119
+ this.allItems();
120
+ if (this.isStreaming()) this.applyFilter();
121
+ });
122
+ }
123
+ urlParentId = signal("", ...ngDevMode ? [{ debugName: "urlParentId" }] : /* istanbul ignore next */ []);
124
+ urlParentType = signal("", ...ngDevMode ? [{ debugName: "urlParentType" }] : /* istanbul ignore next */ []);
125
+ urlFilters = signal([], ...ngDevMode ? [{ debugName: "urlFilters" }] : /* istanbul ignore next */ []);
126
+ activeUrlFilters = computed(() => this.grid()?.appliedPresetFilters() ?? [], ...ngDevMode ? [{ debugName: "activeUrlFilters" }] : /* istanbul ignore next */ []);
127
+ static reservedParams = /* @__PURE__ */ new Set([
128
+ "deleted",
129
+ "parentId",
130
+ "parentType",
131
+ "queryId",
132
+ "parentDeleted"
133
+ ]);
134
+ applyUrlScope(query) {
135
+ const parentId = query.get("parentId") ?? "";
136
+ const parentType = query.get("parentType") ?? "";
137
+ const both = !!parentId && !!parentType;
138
+ if (this.urlParentId() !== (both ? parentId : "")) this.urlParentId.set(both ? parentId : "");
139
+ if (this.urlParentType() !== (both ? parentType : "")) this.urlParentType.set(both ? parentType : "");
140
+ const filters = query.keys.filter((key) => !SparkQueryListComponent.reservedParams.has(key)).map((key) => ({
141
+ name: key,
142
+ includes: query.getAll(key).filter((v) => v !== "")
143
+ })).filter((f) => f.includes.length > 0);
144
+ if (JSON.stringify(filters) !== JSON.stringify(this.urlFilters())) this.urlFilters.set(filters);
145
+ }
146
+ clearUrlFilter(name) {
147
+ this.router.navigate([], {
148
+ relativeTo: this.route,
149
+ queryParams: { [name]: null },
150
+ queryParamsHandling: "merge",
151
+ replaceUrl: true
152
+ });
153
+ }
154
+ clearUrlScope() {
155
+ const queryParams = {
156
+ parentId: null,
157
+ parentType: null
158
+ };
159
+ for (const filter of this.urlFilters()) queryParams[filter.name] = null;
160
+ this.router.navigate([], {
161
+ relativeTo: this.route,
162
+ queryParams,
163
+ queryParamsHandling: "merge",
164
+ replaceUrl: true
165
+ });
166
+ }
167
+ filterValues(filter) {
168
+ return (filter.includes ?? []).map((v) => String(v)).join(", ");
169
+ }
170
+ filterLabel(name) {
171
+ const attribute = this.entityType()?.attributes?.find((a) => a.name === name);
172
+ return this.lang.resolve(attribute?.label) || name;
173
+ }
174
+ async onParamsChange(params) {
175
+ this.errorMessage.set(null);
176
+ this.queryId.set(null);
177
+ this.allItems.set([]);
178
+ this.streamItems.set([]);
179
+ this.streamColumns.set([]);
180
+ this.disconnectStreaming();
181
+ const queryId = params.get("queryId");
182
+ const typeParam = params.get("type");
183
+ if (queryId) {
184
+ this.queryId.set(queryId);
185
+ return;
186
+ }
187
+ if (!typeParam) return;
188
+ const [entityTypes, queries] = await Promise.all([this.sparkService.getEntityTypes(), this.sparkService.getQueries()]);
189
+ const entityType = entityTypes.find((t) => t.id === typeParam || t.alias === typeParam);
190
+ if (!entityType) {
191
+ this.reportLoadFailure({ status: 404 });
192
+ return;
193
+ }
194
+ const singularName = entityType.name;
195
+ const match = queries.find((q) => {
196
+ if (q.entityType === singularName) return true;
197
+ const sourceName = q.source.includes(".") ? q.source.substring(q.source.indexOf(".") + 1) : q.source;
198
+ return sourceName === singularName || sourceName === singularName + "s";
199
+ });
200
+ if (match) this.queryId.set(match.alias || match.id);
201
+ else this.reportLoadFailure({ status: 404 });
202
+ }
203
+ reportLoadFailure(err) {
204
+ this.errorMessage.set(err?.status === 404 ? this.lang.t("spark.query.unavailable") || "This list is not available." : err?.error?.error || err?.message || "An unexpected error occurred");
205
+ }
206
+ async onCustomAction(action) {
207
+ await this.grid()?.onCustomAction(action);
208
+ }
209
+ onCreate() {
210
+ this.createClicked.emit();
211
+ const et = this.entityType();
212
+ if (et) this.router.navigate([
213
+ "/po",
214
+ et.alias || et.id,
215
+ "new"
216
+ ]);
217
+ }
218
+ clearSearch() {
219
+ this.searchTerm.set("");
220
+ }
221
+ connectStreaming(queryId) {
222
+ if (this.streamingSub) return;
223
+ this.isStreaming.set(true);
224
+ this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({
225
+ next: (message) => this.handleStreamingMessage(message),
226
+ error: (err) => {
227
+ this.errorMessage.set(err?.message || "Streaming connection failed");
228
+ this.isStreaming.set(false);
229
+ },
230
+ complete: () => this.isStreaming.set(false)
231
+ });
232
+ }
233
+ disconnectStreaming() {
234
+ if (this.streamingSub) {
235
+ this.streamingSub.unsubscribe();
236
+ this.streamingSub = null;
237
+ }
238
+ this.isStreaming.set(false);
239
+ }
240
+ handleStreamingMessage(message) {
241
+ switch (message.type) {
242
+ case "snapshot":
243
+ this.errorMessage.set(null);
244
+ this.streamColumns.set(message.columns);
245
+ this.allItems.set(message.data);
246
+ break;
247
+ case "patch":
248
+ if (message.updated.length > 0) this.allItems.update((items) => items.map((item) => {
249
+ const patch = message.updated.find((u) => u.id === item.id);
250
+ if (!patch) return item;
251
+ return {
252
+ ...item,
253
+ values: item.values.map((v) => v.key in patch.values ? {
254
+ ...v,
255
+ value: patch.values[v.key]
256
+ } : v)
257
+ };
258
+ }));
259
+ break;
260
+ case "error": this.errorMessage.set(message.message);
261
+ }
262
+ }
263
+ applyFilter() {
264
+ let items = this.allItems();
265
+ const term = this.searchTerm().toLowerCase();
266
+ if (term) items = items.filter((item) => item.values.some((v) => String(v.value ?? "").toLowerCase().includes(term)));
267
+ const sortCols = this.grid()?.settings().sortColumns ?? [];
268
+ if (sortCols.length > 0) items = [...items].sort((a, b) => {
269
+ for (const col of sortCols) {
270
+ const aVal = a.values.find((v) => v.key === col.property)?.value ?? "";
271
+ const bVal = b.values.find((v) => v.key === col.property)?.value ?? "";
272
+ const cmp = String(aVal).localeCompare(String(bVal));
273
+ if (cmp !== 0) return col.direction === "descending" ? -cmp : cmp;
274
+ }
275
+ return 0;
276
+ });
277
+ this.streamItems.set(items);
278
+ }
279
+ static ɵfac = i0.ɵɵngDeclareFactory({
280
+ minVersion: "12.0.0",
281
+ version: "22.2.0",
282
+ ngImport: i0,
283
+ type: SparkQueryListComponent,
284
+ deps: [],
285
+ target: i0.ɵɵFactoryTarget.Component
286
+ });
287
+ static ɵcmp = i0.ɵɵngDeclareComponent({
288
+ minVersion: "17.0.0",
289
+ version: "22.2.0",
290
+ type: SparkQueryListComponent,
291
+ isStandalone: true,
292
+ selector: "spark-query-list",
293
+ inputs: {
294
+ extraActionsTemplate: {
295
+ classPropertyName: "extraActionsTemplate",
296
+ publicName: "extraActionsTemplate",
297
+ isSignal: true,
298
+ isRequired: false,
299
+ transformFunction: null
300
+ },
301
+ showCustomActions: {
302
+ classPropertyName: "showCustomActions",
303
+ publicName: "showCustomActions",
304
+ isSignal: true,
305
+ isRequired: false,
306
+ transformFunction: null
307
+ },
308
+ rowRoute: {
309
+ classPropertyName: "rowRoute",
310
+ publicName: "rowRoute",
311
+ isSignal: true,
312
+ isRequired: false,
313
+ transformFunction: null
314
+ }
315
+ },
316
+ outputs: {
317
+ rowClicked: "rowClicked",
318
+ createClicked: "createClicked",
319
+ customActionExecuted: "customActionExecuted"
320
+ },
321
+ host: { properties: { "class.virtual-scrolling": "isVirtualScrolling()" } },
322
+ viewQueries: [{
323
+ propertyName: "grid",
324
+ first: true,
325
+ predicate: SparkQueryGridComponent,
326
+ descendants: true,
327
+ isSignal: true
328
+ }],
329
+ ngImport: i0,
330
+ template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
331
+ styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--spark-shell-main-bg, var(--bs-body-bg));border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 calc(-1 * var(--spark-main-padding, 1.5rem)) calc(-1 * var(--spark-main-padding, 1.5rem));--spark-content-inset-inline: 0px}:host.virtual-scrolling spark-query-grid{display:flex;flex-direction:column;min-height:0}:host.virtual-scrolling ::ng-deep bs-datatable{display:flex;flex-direction:column;min-height:0;--mp-datatable-virtual-max-height: 100%}:host.virtual-scrolling ::ng-deep bs-datatable mp-datatable{flex:1 1 auto;min-height:0}tr:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.05)}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}.spark-live-badge{font-size:.6em}.spark-url-filter-clear{background:none;border:0;padding:0 0 0 .25rem;color:inherit;line-height:1}\n"],
332
+ dependencies: [
333
+ {
334
+ kind: "component",
335
+ type: BsBadgeComponent,
336
+ selector: "bs-badge",
337
+ inputs: [
338
+ "type",
339
+ "unit",
340
+ "decorative"
341
+ ]
342
+ },
343
+ {
344
+ kind: "ngmodule",
345
+ type: CommonModule
346
+ },
347
+ {
348
+ kind: "directive",
349
+ type: i1.NgComponentOutlet,
350
+ selector: "[ngComponentOutlet]",
351
+ inputs: [
352
+ "ngComponentOutlet",
353
+ "ngComponentOutletInputs",
354
+ "ngComponentOutletInjector",
355
+ "ngComponentOutletEnvironmentInjector",
356
+ "ngComponentOutletContent",
357
+ "ngComponentOutletNgModule"
358
+ ],
359
+ exportAs: ["ngComponentOutlet"]
360
+ },
361
+ {
362
+ kind: "directive",
363
+ type: i1.NgTemplateOutlet,
364
+ selector: "[ngTemplateOutlet]",
365
+ inputs: [
366
+ "ngTemplateOutletContext",
367
+ "ngTemplateOutlet",
368
+ "ngTemplateOutletInjector"
369
+ ]
370
+ },
371
+ {
372
+ kind: "component",
373
+ type: BsAlertComponent,
374
+ selector: "bs-alert",
375
+ inputs: [
376
+ "type",
377
+ "announce",
378
+ "isVisible"
379
+ ],
380
+ outputs: ["isVisibleChange", "afterOpenedOrClosed"]
381
+ },
382
+ {
383
+ kind: "component",
384
+ type: BsGridComponent,
385
+ selector: "bs-grid",
386
+ inputs: ["stopFullWidthAt"]
387
+ },
388
+ {
389
+ kind: "directive",
390
+ type: BsGridRowDirective,
391
+ selector: "[bsRow]"
392
+ },
393
+ {
394
+ kind: "directive",
395
+ type: BsGridColumnDirective,
396
+ selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]",
397
+ inputs: [
398
+ "xxs",
399
+ "xs",
400
+ "sm",
401
+ "md",
402
+ "lg",
403
+ "xl",
404
+ "xxl"
405
+ ]
406
+ },
407
+ {
408
+ kind: "component",
409
+ type: BsPriorityNavComponent,
410
+ selector: "bs-priority-nav",
411
+ inputs: [
412
+ "moreLabel",
413
+ "moreLabelTemplate",
414
+ "collapseAt",
415
+ "overflowFrom",
416
+ "hideEmptyMore",
417
+ "ariaLabel"
418
+ ],
419
+ outputs: ["overflowChange"]
420
+ },
421
+ {
422
+ kind: "directive",
423
+ type: BsPriorityNavItemDirective,
424
+ selector: "[bsPriorityNavItem]",
425
+ inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"]
426
+ },
427
+ {
428
+ kind: "component",
429
+ type: BsSpinnerComponent,
430
+ selector: "bs-spinner",
431
+ inputs: ["type", "color"]
432
+ },
433
+ {
434
+ kind: "component",
435
+ type: SparkIconComponent,
436
+ selector: "spark-icon",
437
+ inputs: ["name"]
438
+ },
439
+ {
440
+ kind: "component",
441
+ type: SparkQueryGridComponent,
442
+ selector: "spark-query-grid",
443
+ inputs: [
444
+ "queryId",
445
+ "parentId",
446
+ "parentType",
447
+ "presetFilters",
448
+ "data",
449
+ "search",
450
+ "columns",
451
+ "reloadToken",
452
+ "deleted",
453
+ "parentDeleted",
454
+ "settings",
455
+ "selection",
456
+ "rowRoute",
457
+ "selectionModeSetting"
458
+ ],
459
+ outputs: [
460
+ "settingsChange",
461
+ "selectionChange",
462
+ "error",
463
+ "rowClicked",
464
+ "customActionExecuted",
465
+ "createClicked"
466
+ ]
467
+ },
468
+ {
469
+ kind: "component",
470
+ type: SparkSearchBoxComponent,
471
+ selector: "spark-search-box",
472
+ inputs: [
473
+ "term",
474
+ "size",
475
+ "debounce"
476
+ ],
477
+ outputs: ["termChange"]
478
+ },
479
+ {
480
+ kind: "pipe",
481
+ type: ResolveTranslationPipe,
482
+ name: "resolveTranslation"
483
+ },
484
+ {
485
+ kind: "pipe",
486
+ type: TranslateKeyPipe,
487
+ name: "t"
488
+ }
489
+ ],
490
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
491
+ });
492
+ };
493
+ i0.ɵɵngDeclareClassMetadata({
494
+ minVersion: "12.0.0",
495
+ version: "22.2.0",
496
+ ngImport: i0,
497
+ type: SparkQueryListComponent,
498
+ decorators: [{
499
+ type: Component,
500
+ args: [{
501
+ selector: "spark-query-list",
502
+ imports: [
503
+ BsBadgeComponent,
504
+ CommonModule,
505
+ NgTemplateOutlet,
506
+ NgComponentOutlet,
507
+ BsAlertComponent,
508
+ BsGridComponent,
509
+ BsGridRowDirective,
510
+ BsGridColumnDirective,
511
+ BsPriorityNavComponent,
512
+ BsPriorityNavItemDirective,
513
+ BsSpinnerComponent,
514
+ SparkIconComponent,
515
+ SparkQueryGridComponent,
516
+ SparkSearchBoxComponent,
517
+ ResolveTranslationPipe,
518
+ TranslateKeyPipe
519
+ ],
520
+ changeDetection: ChangeDetectionStrategy.OnPush,
521
+ host: { "[class.virtual-scrolling]": "isVirtualScrolling()" },
522
+ template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
523
+ styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--spark-shell-main-bg, var(--bs-body-bg));border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 calc(-1 * var(--spark-main-padding, 1.5rem)) calc(-1 * var(--spark-main-padding, 1.5rem));--spark-content-inset-inline: 0px}:host.virtual-scrolling spark-query-grid{display:flex;flex-direction:column;min-height:0}:host.virtual-scrolling ::ng-deep bs-datatable{display:flex;flex-direction:column;min-height:0;--mp-datatable-virtual-max-height: 100%}:host.virtual-scrolling ::ng-deep bs-datatable mp-datatable{flex:1 1 auto;min-height:0}tr:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.05)}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}.spark-live-badge{font-size:.6em}.spark-url-filter-clear{background:none;border:0;padding:0 0 0 .25rem;color:inherit;line-height:1}\n"]
524
+ }]
525
+ }],
526
+ ctorParameters: () => [],
527
+ propDecorators: {
528
+ extraActionsTemplate: [{
529
+ type: i0.Input,
530
+ args: [{
531
+ isSignal: true,
532
+ alias: "extraActionsTemplate",
533
+ required: false
534
+ }]
535
+ }],
536
+ showCustomActions: [{
537
+ type: i0.Input,
538
+ args: [{
539
+ isSignal: true,
540
+ alias: "showCustomActions",
541
+ required: false
542
+ }]
543
+ }],
544
+ rowRoute: [{
545
+ type: i0.Input,
546
+ args: [{
547
+ isSignal: true,
548
+ alias: "rowRoute",
549
+ required: false
550
+ }]
551
+ }],
552
+ rowClicked: [{
553
+ type: i0.Output,
554
+ args: ["rowClicked"]
555
+ }],
556
+ createClicked: [{
557
+ type: i0.Output,
558
+ args: ["createClicked"]
559
+ }],
560
+ customActionExecuted: [{
561
+ type: i0.Output,
562
+ args: ["customActionExecuted"]
563
+ }],
564
+ grid: [{
565
+ type: i0.ViewChild,
566
+ args: [i0.forwardRef(() => SparkQueryGridComponent), { isSignal: true }]
567
+ }]
568
+ }
569
+ });
303
570
  export { SparkQueryListComponent };
304
- //# sourceMappingURL=mintplayer-ng-spark-query-list.mjs.map
571
+
572
+ //# sourceMappingURL=mintplayer-ng-spark-query-list.mjs.map