@mintplayer/ng-spark 22.3.0 → 22.5.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.
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs +10 -1
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-grid.mjs +772 -23
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +233 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +31 -5
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +2 -2
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +28 -302
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +2 -2
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +352 -14
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +137 -308
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +6 -4
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +12 -0
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
- package/package.json +2 -2
- package/types/mintplayer-ng-spark-client-operations.d.ts +10 -1
- package/types/mintplayer-ng-spark-grid.d.ts +455 -19
- package/types/mintplayer-ng-spark-models.d.ts +119 -3
- package/types/mintplayer-ng-spark-po-detail.d.ts +18 -139
- package/types/mintplayer-ng-spark-po-form.d.ts +132 -4
- package/types/mintplayer-ng-spark-query-list.d.ts +53 -66
- package/types/mintplayer-ng-spark-services.d.ts +10 -0
|
@@ -1,35 +1,48 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, DestroyRef, input, output, signal,
|
|
2
|
+
import { inject, DestroyRef, input, output, signal, viewChild, computed, effect, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
3
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
4
|
import * as i1 from '@angular/common';
|
|
5
|
-
import { CommonModule, NgTemplateOutlet
|
|
5
|
+
import { CommonModule, NgTemplateOutlet } from '@angular/common';
|
|
6
6
|
import * as i2 from '@angular/forms';
|
|
7
7
|
import { FormsModule } from '@angular/forms';
|
|
8
|
-
import
|
|
9
|
-
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
8
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
10
9
|
import { Color } from '@mintplayer/ng-bootstrap';
|
|
10
|
+
import { BsBadgeComponent } from '@mintplayer/ng-bootstrap/badge';
|
|
11
11
|
import { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';
|
|
12
|
-
import { DatatableSettings, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective } from '@mintplayer/ng-bootstrap/datatable';
|
|
13
12
|
import { BsFormComponent, BsFormControlDirective } from '@mintplayer/ng-bootstrap/form';
|
|
14
13
|
import { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';
|
|
15
14
|
import { BsInputGroupComponent } from '@mintplayer/ng-bootstrap/input-group';
|
|
16
15
|
import { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';
|
|
17
16
|
import { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';
|
|
18
|
-
import { filterQueryActions, selectionModeFor, parseSelectionRule } from '@mintplayer/ng-spark/models';
|
|
19
|
-
import { SparkQueryRefreshService } from '@mintplayer/ng-spark/client-operations';
|
|
20
17
|
import { SparkService, SparkStreamingService, SparkLanguageService } from '@mintplayer/ng-spark/services';
|
|
21
|
-
import { ResolveTranslationPipe, TranslateKeyPipe
|
|
18
|
+
import { ResolveTranslationPipe, TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';
|
|
22
19
|
import { SparkIconComponent } from '@mintplayer/ng-spark/icon';
|
|
23
|
-
import {
|
|
20
|
+
import { SparkQueryGridComponent } from '@mintplayer/ng-spark/grid';
|
|
24
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
|
+
*/
|
|
25
39
|
class SparkQueryListComponent {
|
|
26
40
|
route = inject(ActivatedRoute);
|
|
27
41
|
router = inject(Router);
|
|
28
42
|
sparkService = inject(SparkService);
|
|
29
43
|
streamingService = inject(SparkStreamingService);
|
|
30
|
-
lang = inject(SparkLanguageService);
|
|
31
|
-
gridRenderers = inject(SparkGridRenderers);
|
|
32
44
|
destroyRef = inject(DestroyRef);
|
|
45
|
+
lang = inject(SparkLanguageService);
|
|
33
46
|
extraActionsTemplate = input(null, /* @ts-ignore */
|
|
34
47
|
...(ngDevMode ? [{ debugName: "extraActionsTemplate" }] : /* istanbul ignore next */ []));
|
|
35
48
|
showCustomActions = input(true, /* @ts-ignore */
|
|
@@ -38,25 +51,40 @@ class SparkQueryListComponent {
|
|
|
38
51
|
createClicked = output();
|
|
39
52
|
customActionExecuted = output();
|
|
40
53
|
colors = Color;
|
|
54
|
+
/** The query the grid should render, resolved from the route. Null until it is known. */
|
|
55
|
+
queryId = signal(null, /* @ts-ignore */
|
|
56
|
+
...(ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []));
|
|
41
57
|
errorMessage = signal(null, /* @ts-ignore */
|
|
42
58
|
...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
|
|
43
|
-
|
|
59
|
+
searchTerm = signal('', /* @ts-ignore */
|
|
60
|
+
...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
|
|
61
|
+
grid = viewChild(SparkQueryGridComponent, /* @ts-ignore */
|
|
62
|
+
...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
|
|
63
|
+
/**
|
|
64
|
+
* Grid state, surfaced for this page's chrome.
|
|
65
|
+
*
|
|
66
|
+
* Optional `viewChild`, read defensively: the action bar and caption render above the grid, so
|
|
67
|
+
* on the first change-detection pass the query has not resolved yet.
|
|
68
|
+
*/
|
|
69
|
+
query = computed(() => this.grid()?.query() ?? null, /* @ts-ignore */
|
|
44
70
|
...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
|
|
45
|
-
entityType =
|
|
71
|
+
entityType = computed(() => this.grid()?.entityType() ?? null, /* @ts-ignore */
|
|
46
72
|
...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
|
|
47
|
-
|
|
48
|
-
...(ngDevMode ? [{ debugName: "allEntityTypes" }] : /* istanbul ignore next */ []));
|
|
49
|
-
lookupReferenceOptions = signal({}, /* @ts-ignore */
|
|
50
|
-
...(ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : /* istanbul ignore next */ []));
|
|
51
|
-
resultCount = signal(null, /* @ts-ignore */
|
|
52
|
-
...(ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []));
|
|
53
|
-
searchTerm = '';
|
|
54
|
-
canRead = signal(false, /* @ts-ignore */
|
|
55
|
-
...(ngDevMode ? [{ debugName: "canRead" }] : /* istanbul ignore next */ []));
|
|
56
|
-
canCreate = signal(false, /* @ts-ignore */
|
|
57
|
-
...(ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []));
|
|
58
|
-
customActions = signal([], /* @ts-ignore */
|
|
73
|
+
customActions = computed(() => this.grid()?.customActions() ?? [], /* @ts-ignore */
|
|
59
74
|
...(ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []));
|
|
75
|
+
canCreate = computed(() => this.grid()?.canCreate() ?? false, /* @ts-ignore */
|
|
76
|
+
...(ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []));
|
|
77
|
+
resultCount = computed(() => this.grid()?.resultCount() ?? null, /* @ts-ignore */
|
|
78
|
+
...(ngDevMode ? [{ debugName: "resultCount" }] : /* istanbul ignore next */ []));
|
|
79
|
+
isVirtualScrolling = computed(() => this.grid()?.isVirtualScrolling() ?? false, /* @ts-ignore */
|
|
80
|
+
...(ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []));
|
|
81
|
+
gridError = computed(() => this.grid()?.errorMessage() ?? null, /* @ts-ignore */
|
|
82
|
+
...(ngDevMode ? [{ debugName: "gridError" }] : /* istanbul ignore next */ []));
|
|
83
|
+
/** Whether an action's selection rule is satisfied. Delegated: the grid holds the selection. */
|
|
84
|
+
isActionEnabled(action) {
|
|
85
|
+
return this.grid()?.isActionEnabled(action) ?? false;
|
|
86
|
+
}
|
|
87
|
+
// --- streaming ---------------------------------------------------------------
|
|
60
88
|
isStreaming = signal(false, /* @ts-ignore */
|
|
61
89
|
...(ngDevMode ? [{ debugName: "isStreaming" }] : /* istanbul ignore next */ []));
|
|
62
90
|
streamingSub = null;
|
|
@@ -64,118 +92,86 @@ class SparkQueryListComponent {
|
|
|
64
92
|
...(ngDevMode ? [{ debugName: "allItems" }] : /* istanbul ignore next */ []));
|
|
65
93
|
streamItems = signal([], /* @ts-ignore */
|
|
66
94
|
...(ngDevMode ? [{ debugName: "streamItems" }] : /* istanbul ignore next */ []));
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
...(ngDevMode ? [{ debugName: "
|
|
95
|
+
/**
|
|
96
|
+
* Rows handed to the grid, or `null` to let it fetch for itself.
|
|
97
|
+
*
|
|
98
|
+
* Null for a normal query — an empty array would read as "here are no rows" and suppress the
|
|
99
|
+
* fetch entirely.
|
|
100
|
+
*/
|
|
101
|
+
gridData = computed(() => this.query()?.isStreamingQuery ? this.streamItems() : null, /* @ts-ignore */
|
|
102
|
+
...(ngDevMode ? [{ debugName: "gridData" }] : /* istanbul ignore next */ []));
|
|
75
103
|
constructor() {
|
|
76
|
-
// The handler is async and this is a subscribe, so a rejection lands nowhere:
|
|
77
|
-
// the metadata load would reject, entityType() would stay null, and the template
|
|
78
|
-
// would render a spinner FOREVER -- while this component has had an errorMessage
|
|
79
|
-
// surface all along that only the fetch path ever reached. Catch it here.
|
|
80
104
|
this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => {
|
|
105
|
+
// The handler is async and this is a subscribe, so a rejection lands nowhere: the metadata
|
|
106
|
+
// load would reject, the query would stay null, and the template would render a spinner
|
|
107
|
+
// FOREVER — while this component has had an errorMessage surface all along that only the
|
|
108
|
+
// fetch path ever reached.
|
|
81
109
|
this.onParamsChange(params).catch((e) => this.reportLoadFailure(e));
|
|
82
110
|
});
|
|
83
111
|
this.destroyRef.onDestroy(() => this.disconnectStreaming());
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
112
|
+
// Connect the socket once the grid has resolved a streaming query, and disconnect whenever it
|
|
113
|
+
// resolves anything else. The grid knows not to fetch for a streaming query, so there is no
|
|
114
|
+
// window in which both transports are live.
|
|
87
115
|
effect(() => {
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
116
|
+
const q = this.query();
|
|
117
|
+
if (q?.isStreamingQuery) {
|
|
118
|
+
this.connectStreaming(q.id);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.disconnectStreaming();
|
|
92
122
|
}
|
|
93
|
-
|
|
123
|
+
});
|
|
124
|
+
// Client-side filter and sort for the streaming snapshot. The server never sees these: there
|
|
125
|
+
// is no request to attach them to.
|
|
126
|
+
effect(() => {
|
|
127
|
+
this.searchTerm();
|
|
128
|
+
this.grid()?.settings();
|
|
129
|
+
this.allItems();
|
|
130
|
+
if (this.isStreaming())
|
|
131
|
+
this.applyFilter();
|
|
94
132
|
});
|
|
95
133
|
}
|
|
96
134
|
async onParamsChange(params) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.entityType.set(null);
|
|
100
|
-
this.fetchFn.set(null);
|
|
101
|
-
this.resultCount.set(null);
|
|
135
|
+
this.errorMessage.set(null);
|
|
136
|
+
this.queryId.set(null);
|
|
102
137
|
this.allItems.set([]);
|
|
103
138
|
this.streamItems.set([]);
|
|
104
|
-
this.errorMessage.set(null);
|
|
105
|
-
// Reset the permission-derived state too. Without this, navigating A -> B where
|
|
106
|
-
// B's load fails leaves A's buttons and A's canRead/canCreate on screen.
|
|
107
|
-
this.canRead.set(false);
|
|
108
|
-
this.canCreate.set(false);
|
|
109
|
-
this.customActions.set([]);
|
|
110
|
-
// Ids from the previous query are meaningless against the next one, and would be POSTed
|
|
111
|
-
// as though they belonged to it.
|
|
112
|
-
this.selection.set([]);
|
|
113
139
|
this.disconnectStreaming();
|
|
114
140
|
const queryId = params.get('queryId');
|
|
115
141
|
const typeParam = params.get('type');
|
|
116
|
-
let resolvedQuery = null;
|
|
117
|
-
let resolvedEntityType = null;
|
|
118
|
-
let resolvedEntityTypes = [];
|
|
119
142
|
if (queryId) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const result = await this.resolveEntityTypeForQuery(resolvedQuery);
|
|
123
|
-
resolvedEntityType = result.entityType;
|
|
124
|
-
resolvedEntityTypes = result.entityTypes;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
else if (typeParam) {
|
|
128
|
-
resolvedEntityTypes = await this.sparkService.getEntityTypes();
|
|
129
|
-
resolvedEntityType = resolvedEntityTypes.find(t => t.id === typeParam || t.alias === typeParam) || null;
|
|
130
|
-
if (resolvedEntityType) {
|
|
131
|
-
const queries = await this.sparkService.getQueries();
|
|
132
|
-
const singularName = resolvedEntityType.name;
|
|
133
|
-
resolvedQuery = queries.find(q => {
|
|
134
|
-
// Match by explicit entityType
|
|
135
|
-
if (q.entityType === singularName)
|
|
136
|
-
return true;
|
|
137
|
-
// Match by source name
|
|
138
|
-
const sourceName = this.extractSourceName(q.source);
|
|
139
|
-
const sourceSingular = this.singularize(sourceName);
|
|
140
|
-
return sourceName === singularName ||
|
|
141
|
-
sourceSingular === singularName ||
|
|
142
|
-
sourceName === singularName + 's';
|
|
143
|
-
}) || null;
|
|
144
|
-
}
|
|
143
|
+
this.queryId.set(queryId);
|
|
144
|
+
return;
|
|
145
145
|
}
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// fetchFn per page. Virtual scrolling is just the [virtualScroll] template flag —
|
|
159
|
-
// the datatable front-loads all pages from fetchFn when virtual.
|
|
160
|
-
this.fetchFn.set(this.makeFetch(resolvedQuery));
|
|
161
|
-
}
|
|
162
|
-
this.loadLookupReferenceOptions();
|
|
163
|
-
const [permissions, actions] = await Promise.all([
|
|
164
|
-
this.sparkService.getPermissions(resolvedEntityType.id),
|
|
165
|
-
this.sparkService.getCustomActions(resolvedEntityType.id),
|
|
166
|
-
]);
|
|
167
|
-
this.canRead.set(permissions.canRead);
|
|
168
|
-
this.canCreate.set(permissions.canCreate);
|
|
169
|
-
// 'query', not 'list'. The server model and docs/guide-custom-actions.md have
|
|
170
|
-
// always documented "detail" | "query" | "both"; this filter tested for a value
|
|
171
|
-
// nothing emits, so an action authored per the documentation rendered NOWHERE.
|
|
172
|
-
this.customActions.set(filterQueryActions(actions));
|
|
146
|
+
if (!typeParam)
|
|
147
|
+
return;
|
|
148
|
+
// `po/:type` — the route names an entity type, so find the query that lists it. The grid takes
|
|
149
|
+
// a query, and this translation is the only reason it cannot take the route directly.
|
|
150
|
+
const [entityTypes, queries] = await Promise.all([
|
|
151
|
+
this.sparkService.getEntityTypes(),
|
|
152
|
+
this.sparkService.getQueries(),
|
|
153
|
+
]);
|
|
154
|
+
const entityType = entityTypes.find(t => t.id === typeParam || t.alias === typeParam);
|
|
155
|
+
if (!entityType) {
|
|
156
|
+
this.reportLoadFailure({ status: 404 });
|
|
157
|
+
return;
|
|
173
158
|
}
|
|
159
|
+
const singularName = entityType.name;
|
|
160
|
+
const match = queries.find(q => {
|
|
161
|
+
if (q.entityType === singularName)
|
|
162
|
+
return true;
|
|
163
|
+
const sourceName = q.source.includes('.') ? q.source.substring(q.source.indexOf('.') + 1) : q.source;
|
|
164
|
+
return sourceName === singularName || sourceName === singularName + 's';
|
|
165
|
+
});
|
|
166
|
+
if (match)
|
|
167
|
+
this.queryId.set(match.alias || match.id);
|
|
168
|
+
else
|
|
169
|
+
this.reportLoadFailure({ status: 404 });
|
|
174
170
|
}
|
|
175
171
|
/**
|
|
176
|
-
* A load failure has to render, not just be swallowed: a denied query answers 404
|
|
177
|
-
*
|
|
178
|
-
*
|
|
172
|
+
* A load failure has to render, not just be swallowed: a denied query answers 404 (audit M-3, so
|
|
173
|
+
* existence is not leaked), which is indistinguishable from a missing one — hence a deliberately
|
|
174
|
+
* generic message rather than a guess at which it was.
|
|
179
175
|
*/
|
|
180
176
|
reportLoadFailure(err) {
|
|
181
177
|
this.errorMessage.set(err?.status === 404
|
|
@@ -183,176 +179,20 @@ class SparkQueryListComponent {
|
|
|
183
179
|
: (err?.error?.error || err?.message || 'An unexpected error occurred'));
|
|
184
180
|
}
|
|
185
181
|
async onCustomAction(action) {
|
|
186
|
-
|
|
187
|
-
const message = this.lang.t(action.confirmationMessageKey) || 'Are you sure?';
|
|
188
|
-
if (!confirm(message))
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
await this.sparkService.executeCustomAction(this.entityType().id, action.name, undefined, this.selection());
|
|
193
|
-
this.customActionExecuted.emit({ action });
|
|
194
|
-
if (action.refreshOnCompleted) {
|
|
195
|
-
this.reload();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
catch (e) {
|
|
199
|
-
const err = e;
|
|
200
|
-
this.errorMessage.set(err.error?.error || err.message || 'Action failed');
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async resolveEntityTypeForQuery(query) {
|
|
204
|
-
const entityTypes = await this.sparkService.getEntityTypes();
|
|
205
|
-
// If entityType is explicitly set on the query, use it directly
|
|
206
|
-
if (query.entityType) {
|
|
207
|
-
const type = entityTypes.find(t => t.name === query.entityType || t.alias === query.entityType?.toLowerCase());
|
|
208
|
-
return { entityType: type || null, entityTypes };
|
|
209
|
-
}
|
|
210
|
-
// For Database.X sources, extract the property name and try to match
|
|
211
|
-
const sourceName = this.extractSourceName(query.source);
|
|
212
|
-
const singularName = this.singularize(sourceName);
|
|
213
|
-
const type = entityTypes.find(t => t.name === sourceName ||
|
|
214
|
-
t.name === singularName ||
|
|
215
|
-
t.clrType.endsWith(singularName));
|
|
216
|
-
return { entityType: type || null, entityTypes };
|
|
217
|
-
}
|
|
218
|
-
extractSourceName(source) {
|
|
219
|
-
const dotIndex = source.indexOf('.');
|
|
220
|
-
return dotIndex >= 0 ? source.substring(dotIndex + 1) : source;
|
|
221
|
-
}
|
|
222
|
-
singularize(plural) {
|
|
223
|
-
const irregulars = {
|
|
224
|
-
'People': 'Person',
|
|
225
|
-
'Children': 'Child',
|
|
226
|
-
'Men': 'Men',
|
|
227
|
-
'Women': 'Woman'
|
|
228
|
-
};
|
|
229
|
-
if (irregulars[plural])
|
|
230
|
-
return irregulars[plural];
|
|
231
|
-
if (plural.endsWith('ies')) {
|
|
232
|
-
return plural.slice(0, -3) + 'y';
|
|
233
|
-
}
|
|
234
|
-
if (plural.endsWith('es')) {
|
|
235
|
-
return plural.slice(0, -2);
|
|
236
|
-
}
|
|
237
|
-
if (plural.endsWith('s')) {
|
|
238
|
-
return plural.slice(0, -1);
|
|
239
|
-
}
|
|
240
|
-
return plural;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Builds the server-side fetch callback the datatable invokes per page/sort.
|
|
244
|
-
* Reads `searchTerm` live, so a settings change (or a new fetchFn identity)
|
|
245
|
-
* refetches with the current search term.
|
|
246
|
-
*/
|
|
247
|
-
makeFetch(query) {
|
|
248
|
-
return (req) => this.sparkService.executeQuery(query.id, {
|
|
249
|
-
sortColumns: req.sortColumns,
|
|
250
|
-
skip: (req.page - 1) * req.perPage,
|
|
251
|
-
take: req.perPage,
|
|
252
|
-
search: this.searchTerm || undefined,
|
|
253
|
-
}).then(r => {
|
|
254
|
-
this.errorMessage.set(null);
|
|
255
|
-
this.resultCount.set(r.totalRecords);
|
|
256
|
-
return {
|
|
257
|
-
data: r.data,
|
|
258
|
-
totalRecords: r.totalRecords,
|
|
259
|
-
totalPages: Math.ceil(r.totalRecords / req.perPage) || 1,
|
|
260
|
-
perPage: req.perPage,
|
|
261
|
-
page: req.page,
|
|
262
|
-
};
|
|
263
|
-
}).catch((e) => {
|
|
264
|
-
this.errorMessage.set(e.error?.error || e.message || 'An unexpected error occurred');
|
|
265
|
-
this.resultCount.set(0);
|
|
266
|
-
return { data: [], totalRecords: 0, totalPages: 1, perPage: req.perPage, page: req.page };
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Force a refetch (e.g. after a custom action) without changing page/sort.
|
|
271
|
-
*
|
|
272
|
-
* Public so a host can drive it, and named to match
|
|
273
|
-
* `SparkSubQueryComponent.reload()` — the two grids had drifted into having the
|
|
274
|
-
* same mechanism under different names, one of them unreachable.
|
|
275
|
-
*/
|
|
276
|
-
reload() {
|
|
277
|
-
if (this.isStreaming()) {
|
|
278
|
-
this.applyFilter();
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
const q = this.query();
|
|
282
|
-
if (q)
|
|
283
|
-
this.fetchFn.set(this.makeFetch(q));
|
|
284
|
-
}
|
|
285
|
-
onSearchChange() {
|
|
286
|
-
if (this.isStreaming()) {
|
|
287
|
-
this.applyFilter();
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
// Reset to page 1 for the new search.
|
|
291
|
-
const s = this.settings();
|
|
292
|
-
this.settings.set(new DatatableSettings({
|
|
293
|
-
perPage: { values: s.perPage.values, selected: s.perPage.selected },
|
|
294
|
-
page: { values: [1], selected: 1 },
|
|
295
|
-
sortColumns: s.sortColumns,
|
|
296
|
-
}));
|
|
297
|
-
// Re-assign the fetch callback so the datatable refetches even when
|
|
298
|
-
// page/perPage/sort are unchanged. ng-bootstrap 22.4's web component dedupes
|
|
299
|
-
// reloads by {sortColumns, perPage, page}; setting a new fetch identity resets
|
|
300
|
-
// that key (set fetch → _lastReloadKey = null) and forces the reload. makeFetch
|
|
301
|
-
// reads searchTerm live (mirrors refresh()).
|
|
302
|
-
const q = this.query();
|
|
303
|
-
if (q)
|
|
304
|
-
this.fetchFn.set(this.makeFetch(q));
|
|
305
|
-
}
|
|
306
|
-
clearSearch() {
|
|
307
|
-
this.searchTerm = '';
|
|
308
|
-
this.onSearchChange();
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Whether the first column links to a detail page.
|
|
312
|
-
*
|
|
313
|
-
* Declared by the query, because the framework cannot derive it: `Database.*` rows
|
|
314
|
-
* are always real documents, but a `Custom.*` query may return loadable documents
|
|
315
|
-
* (Fleet's Stolen_Cars) or rows fabricated in memory (StreamItems). Absent means
|
|
316
|
-
* navigable -- defaulting Custom.* to false would strip the working links off every
|
|
317
|
-
* custom query that does return documents.
|
|
318
|
-
*/
|
|
319
|
-
/**
|
|
320
|
-
* Rows the user has ticked. Lives here rather than in the datatable so the action bar can
|
|
321
|
-
* read it, and MUST be cleared whenever the source changes — otherwise route A's selection
|
|
322
|
-
* is POSTed as ids of route B's type.
|
|
323
|
-
*/
|
|
324
|
-
selection = signal([], /* @ts-ignore */
|
|
325
|
-
...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
|
|
326
|
-
queryRefresh = inject(SparkQueryRefreshService);
|
|
327
|
-
/** 'none' unless an action is selection-gated, so unaffected grids gain no checkbox column. */
|
|
328
|
-
selectionMode = computed(() => selectionModeFor(this.customActions()), /* @ts-ignore */
|
|
329
|
-
...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
|
|
330
|
-
/** Whether an action's selection rule is satisfied right now. The server checks it again. */
|
|
331
|
-
isActionEnabled(action) {
|
|
332
|
-
return parseSelectionRule(action.selectionRule)(this.selection().length);
|
|
333
|
-
}
|
|
334
|
-
isVirtualScrolling = computed(() => isVirtualScrollingQuery(this.query()), /* @ts-ignore */
|
|
335
|
-
...(ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []));
|
|
336
|
-
visibleAttributes = computed(() => visibleGridAttributes(this.entityType()), /* @ts-ignore */
|
|
337
|
-
...(ngDevMode ? [{ debugName: "visibleAttributes" }] : /* istanbul ignore next */ []));
|
|
338
|
-
getColumnRendererComponent(attr) {
|
|
339
|
-
return this.gridRenderers.columnComponentFor(attr);
|
|
340
|
-
}
|
|
341
|
-
getColumnRendererInputs(component, item, attr) {
|
|
342
|
-
return this.gridRenderers.columnInputsFor(component, item, attr);
|
|
343
|
-
}
|
|
344
|
-
async loadLookupReferenceOptions() {
|
|
345
|
-
this.lookupReferenceOptions.set(await this.gridRenderers.loadLookupOptions(this.visibleAttributes()));
|
|
182
|
+
await this.grid()?.onCustomAction(action);
|
|
346
183
|
}
|
|
347
184
|
onCreate() {
|
|
348
185
|
this.createClicked.emit();
|
|
349
186
|
const et = this.entityType();
|
|
350
|
-
if (et)
|
|
187
|
+
if (et)
|
|
351
188
|
this.router.navigate(['/po', et.alias || et.id, 'new']);
|
|
352
|
-
|
|
189
|
+
}
|
|
190
|
+
clearSearch() {
|
|
191
|
+
this.searchTerm.set('');
|
|
353
192
|
}
|
|
354
193
|
connectStreaming(queryId) {
|
|
355
|
-
this.
|
|
194
|
+
if (this.streamingSub)
|
|
195
|
+
return;
|
|
356
196
|
this.isStreaming.set(true);
|
|
357
197
|
this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({
|
|
358
198
|
next: (message) => this.handleStreamingMessage(message),
|
|
@@ -360,9 +200,7 @@ class SparkQueryListComponent {
|
|
|
360
200
|
this.errorMessage.set(err?.message || 'Streaming connection failed');
|
|
361
201
|
this.isStreaming.set(false);
|
|
362
202
|
},
|
|
363
|
-
complete: () =>
|
|
364
|
-
this.isStreaming.set(false);
|
|
365
|
-
}
|
|
203
|
+
complete: () => this.isStreaming.set(false),
|
|
366
204
|
});
|
|
367
205
|
}
|
|
368
206
|
disconnectStreaming() {
|
|
@@ -377,26 +215,18 @@ class SparkQueryListComponent {
|
|
|
377
215
|
case 'snapshot':
|
|
378
216
|
this.errorMessage.set(null);
|
|
379
217
|
this.allItems.set(message.data);
|
|
380
|
-
this.applyFilter();
|
|
381
218
|
break;
|
|
382
219
|
case 'patch':
|
|
383
220
|
if (message.updated.length > 0) {
|
|
384
|
-
|
|
385
|
-
const updatedItems = currentItems.map(item => {
|
|
221
|
+
this.allItems.update(items => items.map(item => {
|
|
386
222
|
const patch = message.updated.find(u => u.id === item.id);
|
|
387
223
|
if (!patch)
|
|
388
224
|
return item;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return attr;
|
|
395
|
-
});
|
|
396
|
-
return { ...item, attributes: updatedAttributes };
|
|
397
|
-
});
|
|
398
|
-
this.allItems.set(updatedItems);
|
|
399
|
-
this.applyFilter();
|
|
225
|
+
return {
|
|
226
|
+
...item,
|
|
227
|
+
attributes: item.attributes.map(attr => attr.name in patch.attributes ? { ...attr, value: patch.attributes[attr.name] } : attr),
|
|
228
|
+
};
|
|
229
|
+
}));
|
|
400
230
|
}
|
|
401
231
|
break;
|
|
402
232
|
case 'error':
|
|
@@ -406,14 +236,14 @@ class SparkQueryListComponent {
|
|
|
406
236
|
}
|
|
407
237
|
applyFilter() {
|
|
408
238
|
let items = this.allItems();
|
|
409
|
-
|
|
410
|
-
if (
|
|
411
|
-
const term = this.searchTerm.toLowerCase();
|
|
239
|
+
const term = this.searchTerm().toLowerCase();
|
|
240
|
+
if (term) {
|
|
412
241
|
items = items.filter(item => item.attributes.some(a => String(a.value ?? '').toLowerCase().includes(term)));
|
|
413
242
|
}
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
|
|
243
|
+
// The datatable in `[data]` mode also sorts on header clicks, but the sort must survive a
|
|
244
|
+
// patch: re-deriving from `allItems` without re-applying it would silently reorder the grid
|
|
245
|
+
// under the user on every update.
|
|
246
|
+
const sortCols = this.grid()?.settings().sortColumns ?? [];
|
|
417
247
|
if (sortCols.length > 0) {
|
|
418
248
|
items = [...items].sort((a, b) => {
|
|
419
249
|
for (const col of sortCols) {
|
|
@@ -427,17 +257,16 @@ class SparkQueryListComponent {
|
|
|
427
257
|
});
|
|
428
258
|
}
|
|
429
259
|
this.streamItems.set(items);
|
|
430
|
-
this.resultCount.set(items.length);
|
|
431
260
|
}
|
|
432
261
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
433
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", 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 } }, outputs: { rowClicked: "rowClicked", createClicked: "createClicked", customActionExecuted: "customActionExecuted" }, host: { properties: { "class.virtual-scrolling": "isVirtualScrolling()" } }, ngImport: i0, template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\" [class.px-3]=\"!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=\"btn btn-outline-primary\"\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 <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <span class=\"badge bg-success ms-2\" style=\"font-size: 0.5em; vertical-align: middle;\">LIVE</span>\n }\n </h2>\n\n @if (entityType()) {\n <!-- Search box -->\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]=\"4\">\n <bs-input-group>\n <span class=\"input-group-text\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'common.search' | t\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange()\">\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 <div [md]=\"8\" class=\"text-end\">\n @if (searchTerm && resultCount() !== null) {\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n }\n </div>\n </div>\n </bs-grid>\n </bs-form>\n\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n </div>\n\n @if (isStreaming()) {\n <bs-datatable class=\"flex-grow-1\"\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"streamItems()\"\n [(settings)]=\"settings\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n <ng-container *ngTemplateOutlet=\"rowCells; context: { $implicit: item }\"></ng-container>\n </ng-container>\n </bs-datatable>\n } @else {\n <bs-datatable class=\"flex-grow-1\"\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [fetch]=\"fetchFn()\"\n [(settings)]=\"settings\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n <ng-container *ngTemplateOutlet=\"rowCells; context: { $implicit: item }\"></ng-container>\n </ng-container>\n </bs-datatable>\n }\n } @else if (errorMessage(); as err) {\n <!-- The metadata load failed. This used to be unreachable: the rejection was\n swallowed by an async subscribe handler, so the spinner below span forever. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n\n<!--\n One row template for both the streaming and the paged datatable. These were two\n byte-identical copies in the same file, which is how the first column's link came to\n need fixing in three places instead of one.\n-->\n<ng-template #rowCells let-item>\n @let row = $any(item);\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (row) {\n <!-- `canRead()` is the whole gate, and it is the rights model: `Query` without\n `Read` lists the rows and withholds the link. Withhold `Read` for a query\n whose rows no detail page can load \u2014 a Custom.* query that fabricates rows\n in memory, say. Note cellContent renders INSIDE this anchor, so a custom\n renderer cannot suppress the link \u2014 it would nest a second one. -->\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, row.id]\" (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, attr: attr }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n</ng-template>\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(rendererType, item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else if (attr.dataType === 'Reference' && attr.isArray) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of (attr.name | referenceChips:item); track chip.id) {\n <span class=\"badge rounded-pill border bg-body-secondary text-body px-3 py-2\">{{ chip.label }}</span>\n }\n </span>\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n", styles: [".spark-actionbar{position:sticky;top:0;z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem}.spark-actionbar .btn{border-radius:0}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 -1.5rem -1.5rem}: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}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { 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: "ngmodule", type: RouterModule }, { kind: "directive", type: i3.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["columns", "data", "fetch", "settings", "selectionMode", "selectable", "selection", "rowKey", "resizableColumns", "pagination", "virtualScroll", "itemSize", "virtualBuffer", "isResponsive", "compareWith", "tree", "idKey", "childCountKey", "treeIndent", "expandedIds", "selectionStrategy"], outputs: ["settingsChange", "selectionChange", "rowClick", "rowDblClick", "rowContextMenu", "expandedIdsChange", "rowExpand", "rowCollapse"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]" }, { 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: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }, { kind: "pipe", type: ReferenceChipsPipe, name: "referenceChips" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
262
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.0", 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 } }, 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.px-4]=\"isVirtualScrolling()\" [class.px-3]=\"!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=\"btn btn-outline-primary\"\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 [search]=\"searchTerm()\"\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{position:sticky;top:0;z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem}.spark-actionbar .btn{border-radius:0}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 -1.5rem -1.5rem}: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", "reloadToken", "settings", "selection"], outputs: ["settingsChange", "selectionChange", "error", "rowClicked", "customActionExecuted"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
434
263
|
}
|
|
435
264
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: SparkQueryListComponent, decorators: [{
|
|
436
265
|
type: Component,
|
|
437
|
-
args: [{ selector: 'spark-query-list', imports: [CommonModule, NgTemplateOutlet,
|
|
266
|
+
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: {
|
|
438
267
|
'[class.virtual-scrolling]': 'isVirtualScrolling()'
|
|
439
|
-
}, template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\" [class.px-3]=\"!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=\"btn btn-outline-primary\"\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 <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <
|
|
440
|
-
}], ctorParameters: () => [], propDecorators: { extraActionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraActionsTemplate", required: false }] }], showCustomActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCustomActions", required: false }] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], createClicked: [{ type: i0.Output, args: ["createClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }] } });
|
|
268
|
+
}, template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\" [class.px-3]=\"!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=\"btn btn-outline-primary\"\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 [search]=\"searchTerm()\"\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{position:sticky;top:0;z-index:400;background-color:var(--bs-tertiary-bg);border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem}.spark-actionbar .btn{border-radius:0}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 -1.5rem -1.5rem}: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"] }]
|
|
269
|
+
}], ctorParameters: () => [], propDecorators: { extraActionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraActionsTemplate", required: false }] }], showCustomActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCustomActions", 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 }] }] } });
|
|
441
270
|
|
|
442
271
|
/**
|
|
443
272
|
* Generated bundle index. Do not edit.
|