@mintplayer/ng-spark 22.31.0 → 22.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -1
- package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-account.mjs +18 -18
- package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs +16 -8
- package/fesm2022/mintplayer-ng-spark-auth-auth-bar.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-auth-confirm-email.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-core.mjs +6 -6
- package/fesm2022/mintplayer-ng-spark-auth-external-login.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-forgot-password.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-login.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-pipes.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-register.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-reset-password.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-sign-in.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-auth-two-factor.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs +24 -24
- package/fesm2022/mintplayer-ng-spark-column-filter.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-contributions.mjs +9 -9
- package/fesm2022/mintplayer-ng-spark-grid.mjs +220 -49
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs +9 -9
- package/fesm2022/mintplayer-ng-spark-icon.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-identity-provider-connected-applications.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-identity-provider-core.mjs +9 -9
- package/fesm2022/mintplayer-ng-spark-identity-provider-developers.mjs +6 -6
- package/fesm2022/mintplayer-ng-spark-identity-provider-management.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-models.mjs +44 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs +21 -21
- package/fesm2022/mintplayer-ng-spark-notifications.mjs +687 -0
- package/fesm2022/mintplayer-ng-spark-notifications.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-organizations-accept.mjs +179 -0
- package/fesm2022/mintplayer-ng-spark-organizations-accept.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-organizations.mjs +202 -0
- package/fesm2022/mintplayer-ng-spark-organizations.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +125 -85
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +90 -26
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +84 -12
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +9 -9
- package/fesm2022/mintplayer-ng-spark-pwa.mjs +74 -6
- package/fesm2022/mintplayer-ng-spark-pwa.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +10 -109
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +3 -3
- package/fesm2022/mintplayer-ng-spark-score-renderers.mjs +801 -0
- package/fesm2022/mintplayer-ng-spark-score-renderers.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-services.mjs +424 -72
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-share-page.mjs +334 -0
- package/fesm2022/mintplayer-ng-spark-share-page.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-share.mjs +10 -0
- package/fesm2022/mintplayer-ng-spark-share.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-shell.mjs +38 -36
- package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +9 -9
- package/package.json +25 -1
- package/types/mintplayer-ng-spark-grid.d.ts +77 -26
- package/types/mintplayer-ng-spark-models.d.ts +150 -17
- package/types/mintplayer-ng-spark-notifications.d.ts +188 -0
- package/types/mintplayer-ng-spark-organizations-accept.d.ts +40 -0
- package/types/mintplayer-ng-spark-organizations.d.ts +72 -0
- package/types/mintplayer-ng-spark-pipes.d.ts +10 -0
- package/types/mintplayer-ng-spark-po-detail.d.ts +26 -0
- package/types/mintplayer-ng-spark-po-edit.d.ts +37 -0
- package/types/mintplayer-ng-spark-pwa.d.ts +48 -2
- package/types/mintplayer-ng-spark-query-list.d.ts +8 -23
- package/types/mintplayer-ng-spark-score-renderers.d.ts +340 -0
- package/types/mintplayer-ng-spark-services.d.ts +146 -6
- package/types/mintplayer-ng-spark-share-page.d.ts +67 -0
- package/types/mintplayer-ng-spark-share.d.ts +26 -0
|
@@ -3,13 +3,14 @@ import * as i0 from "@angular/core";
|
|
|
3
3
|
import { ChangeDetectionStrategy, Component, DestroyRef, Directive, Injectable, Injector, TemplateRef, computed, contentChildren, effect, inject, input, linkedSignal, model, output, runInInjectionContext, signal, untracked, viewChild } from "@angular/core";
|
|
4
4
|
import { ShowedOn, confirmationText, defaultQueryActions, filterQueryActions, hasShowedOnFlag, parseSelectionRule, selectionModeFor, valueFor } from "@mintplayer/ng-spark/models";
|
|
5
5
|
import { SPARK_ATTRIBUTE_RENDERERS, cellValue, withDeclaredInputs } from "@mintplayer/ng-spark/renderers";
|
|
6
|
-
import { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkService, sparkDatatableLabels } from "@mintplayer/ng-spark/services";
|
|
6
|
+
import { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkLiveService, SparkService, canonicalJson, describeSparkError, sparkDatatableLabels } from "@mintplayer/ng-spark/services";
|
|
7
7
|
import * as i1$1 from "@angular/common";
|
|
8
8
|
import { CommonModule, NgComponentOutlet } from "@angular/common";
|
|
9
|
+
import { HttpErrorResponse } from "@angular/common/http";
|
|
9
10
|
import { toSignal } from "@angular/core/rxjs-interop";
|
|
10
11
|
import * as i2 from "@angular/router";
|
|
11
12
|
import { NavigationEnd, Router, RouterModule } from "@angular/router";
|
|
12
|
-
import { filter, map } from "rxjs";
|
|
13
|
+
import { BehaviorSubject, filter, map } from "rxjs";
|
|
13
14
|
import { Color } from "@mintplayer/ng-bootstrap";
|
|
14
15
|
import { BsAlertComponent } from "@mintplayer/ng-bootstrap/alert";
|
|
15
16
|
import { BsSpinnerComponent } from "@mintplayer/ng-bootstrap/spinner";
|
|
@@ -86,7 +87,7 @@ var SparkGridRenderers = class SparkGridRenderers {
|
|
|
86
87
|
}
|
|
87
88
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
88
89
|
minVersion: "12.0.0",
|
|
89
|
-
version: "22.2.
|
|
90
|
+
version: "22.2.2",
|
|
90
91
|
ngImport: i0,
|
|
91
92
|
type: SparkGridRenderers,
|
|
92
93
|
deps: [],
|
|
@@ -94,7 +95,7 @@ var SparkGridRenderers = class SparkGridRenderers {
|
|
|
94
95
|
});
|
|
95
96
|
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
96
97
|
minVersion: "12.0.0",
|
|
97
|
-
version: "22.2.
|
|
98
|
+
version: "22.2.2",
|
|
98
99
|
ngImport: i0,
|
|
99
100
|
type: SparkGridRenderers,
|
|
100
101
|
providedIn: "root"
|
|
@@ -102,7 +103,7 @@ var SparkGridRenderers = class SparkGridRenderers {
|
|
|
102
103
|
};
|
|
103
104
|
i0.ɵɵngDeclareClassMetadata({
|
|
104
105
|
minVersion: "12.0.0",
|
|
105
|
-
version: "22.2.
|
|
106
|
+
version: "22.2.2",
|
|
106
107
|
ngImport: i0,
|
|
107
108
|
type: SparkGridRenderers,
|
|
108
109
|
decorators: [{
|
|
@@ -126,7 +127,7 @@ var SparkQueryIconDirective = class SparkQueryIconDirective {
|
|
|
126
127
|
}
|
|
127
128
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
128
129
|
minVersion: "12.0.0",
|
|
129
|
-
version: "22.2.
|
|
130
|
+
version: "22.2.2",
|
|
130
131
|
ngImport: i0,
|
|
131
132
|
type: SparkQueryIconDirective,
|
|
132
133
|
deps: [],
|
|
@@ -134,7 +135,7 @@ var SparkQueryIconDirective = class SparkQueryIconDirective {
|
|
|
134
135
|
});
|
|
135
136
|
static ɵdir = i0.ɵɵngDeclareDirective({
|
|
136
137
|
minVersion: "17.1.0",
|
|
137
|
-
version: "22.2.
|
|
138
|
+
version: "22.2.2",
|
|
138
139
|
type: SparkQueryIconDirective,
|
|
139
140
|
isStandalone: true,
|
|
140
141
|
selector: "[sparkQueryIcon]",
|
|
@@ -150,7 +151,7 @@ var SparkQueryIconDirective = class SparkQueryIconDirective {
|
|
|
150
151
|
};
|
|
151
152
|
i0.ɵɵngDeclareClassMetadata({
|
|
152
153
|
minVersion: "12.0.0",
|
|
153
|
-
version: "22.2.
|
|
154
|
+
version: "22.2.2",
|
|
154
155
|
ngImport: i0,
|
|
155
156
|
type: SparkQueryIconDirective,
|
|
156
157
|
decorators: [{
|
|
@@ -177,7 +178,7 @@ var SparkQueryCaptionDirective = class SparkQueryCaptionDirective {
|
|
|
177
178
|
}
|
|
178
179
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
179
180
|
minVersion: "12.0.0",
|
|
180
|
-
version: "22.2.
|
|
181
|
+
version: "22.2.2",
|
|
181
182
|
ngImport: i0,
|
|
182
183
|
type: SparkQueryCaptionDirective,
|
|
183
184
|
deps: [],
|
|
@@ -185,7 +186,7 @@ var SparkQueryCaptionDirective = class SparkQueryCaptionDirective {
|
|
|
185
186
|
});
|
|
186
187
|
static ɵdir = i0.ɵɵngDeclareDirective({
|
|
187
188
|
minVersion: "17.1.0",
|
|
188
|
-
version: "22.2.
|
|
189
|
+
version: "22.2.2",
|
|
189
190
|
type: SparkQueryCaptionDirective,
|
|
190
191
|
isStandalone: true,
|
|
191
192
|
selector: "[sparkQueryCaption]",
|
|
@@ -201,7 +202,7 @@ var SparkQueryCaptionDirective = class SparkQueryCaptionDirective {
|
|
|
201
202
|
};
|
|
202
203
|
i0.ɵɵngDeclareClassMetadata({
|
|
203
204
|
minVersion: "12.0.0",
|
|
204
|
-
version: "22.2.
|
|
205
|
+
version: "22.2.2",
|
|
205
206
|
ngImport: i0,
|
|
206
207
|
type: SparkQueryCaptionDirective,
|
|
207
208
|
decorators: [{
|
|
@@ -228,7 +229,7 @@ var SparkQueryActionsDirective = class SparkQueryActionsDirective {
|
|
|
228
229
|
}
|
|
229
230
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
230
231
|
minVersion: "12.0.0",
|
|
231
|
-
version: "22.2.
|
|
232
|
+
version: "22.2.2",
|
|
232
233
|
ngImport: i0,
|
|
233
234
|
type: SparkQueryActionsDirective,
|
|
234
235
|
deps: [],
|
|
@@ -236,7 +237,7 @@ var SparkQueryActionsDirective = class SparkQueryActionsDirective {
|
|
|
236
237
|
});
|
|
237
238
|
static ɵdir = i0.ɵɵngDeclareDirective({
|
|
238
239
|
minVersion: "17.1.0",
|
|
239
|
-
version: "22.2.
|
|
240
|
+
version: "22.2.2",
|
|
240
241
|
type: SparkQueryActionsDirective,
|
|
241
242
|
isStandalone: true,
|
|
242
243
|
selector: "[sparkQueryActions]",
|
|
@@ -252,7 +253,7 @@ var SparkQueryActionsDirective = class SparkQueryActionsDirective {
|
|
|
252
253
|
};
|
|
253
254
|
i0.ɵɵngDeclareClassMetadata({
|
|
254
255
|
minVersion: "12.0.0",
|
|
255
|
-
version: "22.2.
|
|
256
|
+
version: "22.2.2",
|
|
256
257
|
ngImport: i0,
|
|
257
258
|
type: SparkQueryActionsDirective,
|
|
258
259
|
decorators: [{
|
|
@@ -335,7 +336,7 @@ var SparkGridCellComponent = class SparkGridCellComponent {
|
|
|
335
336
|
}, ...ngDevMode ? [{ debugName: "dateValue" }] : /* istanbul ignore next */ []);
|
|
336
337
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
337
338
|
minVersion: "12.0.0",
|
|
338
|
-
version: "22.2.
|
|
339
|
+
version: "22.2.2",
|
|
339
340
|
ngImport: i0,
|
|
340
341
|
type: SparkGridCellComponent,
|
|
341
342
|
deps: [],
|
|
@@ -343,7 +344,7 @@ var SparkGridCellComponent = class SparkGridCellComponent {
|
|
|
343
344
|
});
|
|
344
345
|
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
345
346
|
minVersion: "17.0.0",
|
|
346
|
-
version: "22.2.
|
|
347
|
+
version: "22.2.2",
|
|
347
348
|
type: SparkGridCellComponent,
|
|
348
349
|
isStandalone: true,
|
|
349
350
|
selector: "spark-grid-cell",
|
|
@@ -447,7 +448,7 @@ var SparkGridCellComponent = class SparkGridCellComponent {
|
|
|
447
448
|
};
|
|
448
449
|
i0.ɵɵngDeclareClassMetadata({
|
|
449
450
|
minVersion: "12.0.0",
|
|
450
|
-
version: "22.2.
|
|
451
|
+
version: "22.2.2",
|
|
451
452
|
ngImport: i0,
|
|
452
453
|
type: SparkGridCellComponent,
|
|
453
454
|
decorators: [{
|
|
@@ -515,8 +516,74 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
515
516
|
}]
|
|
516
517
|
}
|
|
517
518
|
});
|
|
519
|
+
const LIVE_GRID_SETTLE_MS = 1e3;
|
|
520
|
+
var LiveGridSession = class {
|
|
521
|
+
onPush;
|
|
522
|
+
onError;
|
|
523
|
+
settleMs;
|
|
524
|
+
requests = new BehaviorSubject(null);
|
|
525
|
+
subscription;
|
|
526
|
+
currentKey = null;
|
|
527
|
+
latest = null;
|
|
528
|
+
waiters = [];
|
|
529
|
+
failed = null;
|
|
530
|
+
constructor(live, onPush, onError, settleMs = LIVE_GRID_SETTLE_MS) {
|
|
531
|
+
this.onPush = onPush;
|
|
532
|
+
this.onError = onError;
|
|
533
|
+
this.settleMs = settleMs;
|
|
534
|
+
this.subscription = live.watchQuery(this.requests).subscribe((event) => {
|
|
535
|
+
if (event.type === "result") {
|
|
536
|
+
this.latest = event.result;
|
|
537
|
+
if (this.waiters.length) this.resolveWaiters(event.result);
|
|
538
|
+
else this.onPush();
|
|
539
|
+
} else {
|
|
540
|
+
this.failed = event;
|
|
541
|
+
this.latest = null;
|
|
542
|
+
this.resolveWaiters(null);
|
|
543
|
+
this.onError(event);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
fetch(request) {
|
|
548
|
+
if (this.failed) return Promise.resolve(null);
|
|
549
|
+
const key = canonicalJson(request);
|
|
550
|
+
if (key === this.currentKey) {
|
|
551
|
+
if (this.waiters.length === 0 && this.latest) return Promise.resolve(this.latest);
|
|
552
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
553
|
+
}
|
|
554
|
+
const answered = this.currentKey === null || this.latest === null;
|
|
555
|
+
this.currentKey = key;
|
|
556
|
+
this.requests.next(request);
|
|
557
|
+
return new Promise((resolve) => {
|
|
558
|
+
let timer = null;
|
|
559
|
+
const waiter = (result) => {
|
|
560
|
+
if (timer) clearTimeout(timer);
|
|
561
|
+
resolve(result);
|
|
562
|
+
};
|
|
563
|
+
this.waiters.push(waiter);
|
|
564
|
+
if (!answered) timer = setTimeout(() => this.resolveWaiters(this.latest), this.settleMs);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
dispose() {
|
|
568
|
+
this.subscription.unsubscribe();
|
|
569
|
+
this.requests.complete();
|
|
570
|
+
this.resolveWaiters(null);
|
|
571
|
+
}
|
|
572
|
+
resolveWaiters(result) {
|
|
573
|
+
const waiters = this.waiters;
|
|
574
|
+
this.waiters = [];
|
|
575
|
+
for (const waiter of waiters) waiter(result);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const LIVE_ERROR_STATUS = {
|
|
579
|
+
unavailable: 404,
|
|
580
|
+
invalid: 400,
|
|
581
|
+
limit: 429,
|
|
582
|
+
failed: 500
|
|
583
|
+
};
|
|
518
584
|
var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
519
585
|
sparkService = inject(SparkService);
|
|
586
|
+
liveService = inject(SparkLiveService);
|
|
520
587
|
gridRenderers = inject(SparkGridRenderers);
|
|
521
588
|
queryRefresh = inject(SparkQueryRefreshService);
|
|
522
589
|
injector = inject(Injector);
|
|
@@ -529,6 +596,13 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
529
596
|
presetFilters = input([], ...ngDevMode ? [{ debugName: "presetFilters" }] : /* istanbul ignore next */ []);
|
|
530
597
|
appliedPresetFilters = signal([], ...ngDevMode ? [{ debugName: "appliedPresetFilters" }] : /* istanbul ignore next */ []);
|
|
531
598
|
data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
|
|
599
|
+
live = input(void 0, ...ngDevMode ? [{ debugName: "live" }] : /* istanbul ignore next */ []);
|
|
600
|
+
liveFailed = signal(false, ...ngDevMode ? [{ debugName: "liveFailed" }] : /* istanbul ignore next */ []);
|
|
601
|
+
isLive = computed(() => this.data() === null && this.liveWanted(this.query()) && !this.liveFailed(), ...ngDevMode ? [{ debugName: "isLive" }] : /* istanbul ignore next */ []);
|
|
602
|
+
liveWanted(query) {
|
|
603
|
+
if (!query || isVirtualScrollingQuery(query)) return false;
|
|
604
|
+
return this.live() ?? query.live === true;
|
|
605
|
+
}
|
|
532
606
|
search = input("", ...ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []);
|
|
533
607
|
columns = input(null, ...ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []);
|
|
534
608
|
reloadToken = input(null, ...ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []);
|
|
@@ -846,7 +920,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
846
920
|
allColumns = computed(() => this.columns() ?? this.fetchedColumns(), ...ngDevMode ? [{ debugName: "allColumns" }] : /* istanbul ignore next */ []);
|
|
847
921
|
rowActionsColumn = "__sparkRowActions";
|
|
848
922
|
datatableLabels = computed(() => sparkDatatableLabels(this.lang), ...ngDevMode ? [{ debugName: "datatableLabels" }] : /* istanbul ignore next */ []);
|
|
849
|
-
hasExternalData = computed(() => this.data() !== null
|
|
923
|
+
hasExternalData = computed(() => this.data() !== null, ...ngDevMode ? [{ debugName: "hasExternalData" }] : /* istanbul ignore next */ []);
|
|
850
924
|
isActionEnabled(action) {
|
|
851
925
|
return this.clientMethods.unavailableReason(action.requiresClient) === null && parseSelectionRule(action.selectionRule)(this.selection().map((r) => r.id).length);
|
|
852
926
|
}
|
|
@@ -929,6 +1003,46 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
929
1003
|
this.onFilterChanged();
|
|
930
1004
|
});
|
|
931
1005
|
});
|
|
1006
|
+
let firstLive = true;
|
|
1007
|
+
effect(() => {
|
|
1008
|
+
this.live();
|
|
1009
|
+
if (firstLive) {
|
|
1010
|
+
firstLive = false;
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
untracked(() => {
|
|
1014
|
+
const query = this.query();
|
|
1015
|
+
if (!query || this.hasExternalData()) return;
|
|
1016
|
+
this.disposeLive();
|
|
1017
|
+
this.fetchFn.set(this.makeFetch(query, this.parentId(), this.parentType()));
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
inject(DestroyRef).onDestroy(() => this.disposeLive());
|
|
1021
|
+
}
|
|
1022
|
+
liveSession = null;
|
|
1023
|
+
disposeLive() {
|
|
1024
|
+
this.liveSession?.dispose();
|
|
1025
|
+
this.liveSession = null;
|
|
1026
|
+
this.liveFailed.set(false);
|
|
1027
|
+
}
|
|
1028
|
+
onLivePush() {
|
|
1029
|
+
const datatable = this.datatable();
|
|
1030
|
+
if (datatable && this.fetchFn()) datatable.reload();
|
|
1031
|
+
}
|
|
1032
|
+
onLiveError(error) {
|
|
1033
|
+
this.liveFailed.set(true);
|
|
1034
|
+
this.errorMessage.set(error.code === "unavailable" ? this.lang.t("spark.query.unavailable", "This list is not available.") : error.message || this.lang.t("common.unexpectedError", "An unexpected error occurred"));
|
|
1035
|
+
this.resultCount.set(0);
|
|
1036
|
+
this.loadedIds.set(/* @__PURE__ */ new Set());
|
|
1037
|
+
this.onLivePush();
|
|
1038
|
+
this.error.emit(new HttpErrorResponse({
|
|
1039
|
+
status: LIVE_ERROR_STATUS[error.code] ?? 500,
|
|
1040
|
+
statusText: error.code,
|
|
1041
|
+
error: {
|
|
1042
|
+
error: error.message,
|
|
1043
|
+
code: error.code
|
|
1044
|
+
}
|
|
1045
|
+
}));
|
|
932
1046
|
}
|
|
933
1047
|
reload(options) {
|
|
934
1048
|
if (this.hasExternalData()) return;
|
|
@@ -1034,8 +1148,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1034
1148
|
this.customActionExecuted.emit({ action });
|
|
1035
1149
|
if (action.refreshOnCompleted) this.reload();
|
|
1036
1150
|
} catch (e) {
|
|
1037
|
-
|
|
1038
|
-
this.errorMessage.set(err.error?.error || err.message || this.lang.t("common.actionFailed") || "Action failed");
|
|
1151
|
+
this.errorMessage.set(describeSparkError(e, this.lang, e?.message || this.lang.t("common.actionFailed", "Action failed")));
|
|
1039
1152
|
}
|
|
1040
1153
|
}
|
|
1041
1154
|
reportError(err) {
|
|
@@ -1043,13 +1156,14 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1043
1156
|
this.error.emit(err);
|
|
1044
1157
|
}
|
|
1045
1158
|
describe(err) {
|
|
1046
|
-
if (err?.status === 404) return this.lang.t("spark.query.unavailable"
|
|
1047
|
-
return err?.error?.error || err?.message || this.lang.t("common.unexpectedError"
|
|
1159
|
+
if (err?.status === 404) return this.lang.t("spark.query.unavailable", "This list is not available.");
|
|
1160
|
+
return err?.error?.error || err?.message || this.lang.t("common.unexpectedError", "An unexpected error occurred");
|
|
1048
1161
|
}
|
|
1049
1162
|
async loadData(queryId, parentId, parentType) {
|
|
1050
1163
|
this.loading.set(true);
|
|
1051
1164
|
this.errorMessage.set(null);
|
|
1052
1165
|
this.fetchFn.set(null);
|
|
1166
|
+
this.disposeLive();
|
|
1053
1167
|
this.query.set(null);
|
|
1054
1168
|
this.entityType.set(null);
|
|
1055
1169
|
this.canRead.set(false);
|
|
@@ -1079,7 +1193,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1079
1193
|
this.defaultActions.set(defaultQueryActions(actions));
|
|
1080
1194
|
} else if (resolvedQuery) console.warn(`[spark] Query '${resolvedQuery.name}' names entity type '${resolvedQuery.entityType ?? resolvedQuery.source}', which resolves to no type in /spark/types; the grid offers no actions and no row links.`);
|
|
1081
1195
|
this.settings.set(initialGridSettings(resolvedQuery));
|
|
1082
|
-
if (this.data() === null
|
|
1196
|
+
if (this.data() === null) this.fetchFn.set(this.makeFetch(resolvedQuery, parentId, parentType));
|
|
1083
1197
|
this.loadLookupReferenceOptions();
|
|
1084
1198
|
} catch (e) {
|
|
1085
1199
|
this.fetchFn.set(null);
|
|
@@ -1096,7 +1210,10 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1096
1210
|
return entityTypes.find((t) => t.name === sourceName || t.name === singular || t.name + "s" === sourceName || t.clrType?.endsWith(singular)) ?? null;
|
|
1097
1211
|
}
|
|
1098
1212
|
makeFetch(query, parentId, parentType) {
|
|
1099
|
-
return (
|
|
1213
|
+
return this.liveWanted(query) && this.data() === null ? this.makeLiveFetch(query, parentId, parentType) : this.makeHttpFetch(query, parentId, parentType);
|
|
1214
|
+
}
|
|
1215
|
+
requestFor(req, parentId, parentType) {
|
|
1216
|
+
return {
|
|
1100
1217
|
sortColumns: req.sortColumns,
|
|
1101
1218
|
skip: (req.page - 1) * req.perPage,
|
|
1102
1219
|
take: req.perPage,
|
|
@@ -1106,21 +1223,59 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1106
1223
|
parentType,
|
|
1107
1224
|
...this.effectiveDeleted() ? { deleted: this.effectiveDeleted() } : {},
|
|
1108
1225
|
...this.parentDeleted() && parentId && parentType ? { parentDeleted: "include" } : {}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
toPage(r, req) {
|
|
1229
|
+
this.errorMessage.set(null);
|
|
1230
|
+
this.resultCount.set(r.totalItems);
|
|
1231
|
+
this.setFetchedColumns(r.columns ?? []);
|
|
1232
|
+
this.disabledActions.set(r.disabledActions ?? []);
|
|
1233
|
+
const ids = (r.items ?? []).map((item) => item.id);
|
|
1234
|
+
this.loadedIds.set(this.isVirtualScrolling() ? /* @__PURE__ */ new Set([...this.loadedIds(), ...ids]) : new Set(ids));
|
|
1235
|
+
return {
|
|
1236
|
+
data: r.items ?? [],
|
|
1237
|
+
totalRecords: r.totalItems,
|
|
1238
|
+
totalPages: Math.ceil(r.totalItems / req.perPage) || 1,
|
|
1239
|
+
perPage: req.perPage,
|
|
1240
|
+
page: req.page
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
makeLiveFetch(query, parentId, parentType) {
|
|
1244
|
+
const session = this.liveSession ??= new LiveGridSession(this.liveService, () => this.onLivePush(), (e) => this.onLiveError(e));
|
|
1245
|
+
return async (req) => {
|
|
1246
|
+
const options = this.requestFor(req, parentId, parentType);
|
|
1247
|
+
const request = {
|
|
1248
|
+
queryId: query.id,
|
|
1249
|
+
sortColumns: options.sortColumns?.length ? options.sortColumns.map((c) => ({
|
|
1250
|
+
property: c.property,
|
|
1251
|
+
direction: c.direction === "descending" ? "desc" : "asc"
|
|
1252
|
+
})) : void 0,
|
|
1253
|
+
skip: options.skip,
|
|
1254
|
+
take: options.take,
|
|
1255
|
+
search: options.search,
|
|
1256
|
+
columns: options.columns.length ? options.columns : void 0,
|
|
1257
|
+
parentId: options.parentId || void 0,
|
|
1258
|
+
parentType: options.parentType || void 0,
|
|
1259
|
+
deleted: options.deleted,
|
|
1260
|
+
parentDeleted: options.parentDeleted
|
|
1122
1261
|
};
|
|
1123
|
-
|
|
1262
|
+
const result = await session.fetch(request);
|
|
1263
|
+
if (!result) {
|
|
1264
|
+
this.resultCount.set(0);
|
|
1265
|
+
this.loadedIds.set(/* @__PURE__ */ new Set());
|
|
1266
|
+
return {
|
|
1267
|
+
data: [],
|
|
1268
|
+
totalRecords: 0,
|
|
1269
|
+
totalPages: 1,
|
|
1270
|
+
perPage: req.perPage,
|
|
1271
|
+
page: req.page
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
return this.toPage(result, req);
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
makeHttpFetch(query, parentId, parentType) {
|
|
1278
|
+
return (req) => this.sparkService.executeQuery(query.id, this.requestFor(req, parentId, parentType)).then((r) => this.toPage(r, req)).catch((e) => {
|
|
1124
1279
|
this.reportError(e);
|
|
1125
1280
|
this.resultCount.set(0);
|
|
1126
1281
|
return {
|
|
@@ -1149,7 +1304,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1149
1304
|
}
|
|
1150
1305
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1151
1306
|
minVersion: "12.0.0",
|
|
1152
|
-
version: "22.2.
|
|
1307
|
+
version: "22.2.2",
|
|
1153
1308
|
ngImport: i0,
|
|
1154
1309
|
type: SparkQueryGridComponent,
|
|
1155
1310
|
deps: [],
|
|
@@ -1157,7 +1312,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1157
1312
|
});
|
|
1158
1313
|
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1159
1314
|
minVersion: "17.0.0",
|
|
1160
|
-
version: "22.2.
|
|
1315
|
+
version: "22.2.2",
|
|
1161
1316
|
type: SparkQueryGridComponent,
|
|
1162
1317
|
isStandalone: true,
|
|
1163
1318
|
selector: "spark-query-grid",
|
|
@@ -1197,6 +1352,13 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1197
1352
|
isRequired: false,
|
|
1198
1353
|
transformFunction: null
|
|
1199
1354
|
},
|
|
1355
|
+
live: {
|
|
1356
|
+
classPropertyName: "live",
|
|
1357
|
+
publicName: "live",
|
|
1358
|
+
isSignal: true,
|
|
1359
|
+
isRequired: false,
|
|
1360
|
+
transformFunction: null
|
|
1361
|
+
},
|
|
1200
1362
|
search: {
|
|
1201
1363
|
classPropertyName: "search",
|
|
1202
1364
|
publicName: "search",
|
|
@@ -1277,7 +1439,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1277
1439
|
isSignal: true
|
|
1278
1440
|
}],
|
|
1279
1441
|
ngImport: i0,
|
|
1280
|
-
template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and `query()` is only\n set after loadData's awaits resolve. Three separate bugs fell out of that one line: the spinner\n was unreachable on a first load (it rendered inside the gate, while `loading` starts true and\n `query` starts null), a first-load failure rendered ZERO DOM — no card, no message, nothing to\n see or report — and a failed RELOAD left the previous query's rows on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the final `@else`\n always renders something. Do not fold these back into one gate, and do not move the spinner\n inside the `query()` branch.\n-->\n@if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n} @else if (query()) {\n <!-- A page-fetch failure while the grid is already resolved. Without this the catch returns an\n empty page and a failed query is indistinguishable from one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <!--\n One datatable, two row sources. `[data]` and `[fetch]` are mutually exclusive in\n ng-bootstrap — binding both makes `[fetch]` win silently — so exactly one is ever non-null\n here: `fetchFn` is left null whenever `data` is bound. See the class comment for why rows can\n arrive from outside at all (streaming must not put a websocket in every detail page's bundle).\n -->\n <!--\n The selection bar (#460, M15): the \"N selected\" chip, only while rows are selected, and part\n of the grid rather than of a host, so the query card and the query-list page show the same\n thing. There is deliberately no select-all (owner decision, 2026-09-29): with paged, lazy or\n virtual-scrolled rows it could only tick the rows loaded so far. The selection survives paging\n (#467, D19), so the chip counts every ticked row — exactly what an action receives — and says how\n many are on other pages; its ⊗ drops them all, since the header checkbox only sees this page.\n The bar keeps its height while empty, so ticking the first row does not push the table down\n under the pointer.\n -->\n @if (selectionMode() !== 'none') {\n <div class=\"spark-selection-bar d-flex align-items-center gap-2 mb-2\" style=\"min-height: 1.5rem\">\n @if (selection().length) {\n <bs-badge [type]=\"colors.primary\" class=\"spark-selection-chip\">\n {{ selection().length }} {{ 'common.selected' | t }}\n @if (offPageCount()) {\n <span class=\"spark-selection-off-page\">· {{ offPageCount() }} {{ 'common.onOtherPages' | t }}</span>\n }\n <button type=\"button\" class=\"spark-selection-clear\"\n [attr.aria-label]=\"'common.clearSelection' | t\" (click)=\"clearSelection()\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n </div>\n }\n\n <!-- 'checkbox' (#467, D9): only the checkbox cell selects; a row click opens the row. -->\n <bs-datatable\n [selectionMode]=\"selectionMode() === 'multiple' ? 'checkbox' : 'none'\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n [labels]=\"datatableLabels()\"\n (filterChange)=\"onFilterChange($event)\"\n (rowClick)=\"onRowClick($event)\"\n [(settings)]=\"settings\">\n @for (col of allColumns(); track col.name) {\n <!--\n `canSort`/`canFilter` arrive already resolved by the server (query override → attribute →\n true), so this only spells the polarity. `!== false` is needed for BOTH, for opposite\n reasons: absent means capable on our wire, while the datatable's own `filterable` defaults\n to false, unlike `sortable`.\n\n ⚠️ `filterActive` and `filterSummary` are deliberately NOT bound. Any *bsDatatableColumn\n input change recomputes the column set, which retires the live filter view — while the web\n component's mount-once guard leaves the panel's nodes in place and never re-renders them.\n Binding them to state our own panel mutates would freeze the panel the instant a value is\n ticked, until it is closed and reopened. The cost is that a filtered column carries no\n visual marker yet, which is worth far less than a panel that dies on first use. See #431 F7.\n -->\n <div *bsDatatableColumn=\"col.name; sortable: col.canSort !== false; filterable: col.canFilter !== false; label: (col.label | resolveTranslation) || col.name\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\n\n @if (col.canFilter !== false) {\n <ng-container *bsDatatableFilterPanel=\"let values; let ctx = ctx\">\n <spark-column-filter-panel\n [column]=\"col\"\n [values]=\"values()\"\n [ctx]=\"ctx\"\n [filterLabel]=\"filterLabelFor(col)\" />\n </ng-container>\n }\n </div>\n }\n <!--\n The per-row ⋮ menu's column: no header, never sorted or filtered.\n\n ⚠️ Only once the data columns exist. Those ship WITH the first page, so the rows reach the\n datatable one render before the column templates do. The datatable sizes its columns ONCE,\n from the first render that has both columns and rows, then switches to `table-layout: fixed`\n and never measures again. If this column were already registered it would be the only one\n measured; the data columns would share what it left over equally, and at phone width their\n unclipped headers ran into each other. With no column at all the datatable waits.\n -->\n @if (rowActions().length && allColumns().length) {\n <div *bsDatatableColumn=\"rowActionsColumn; sortable: false; filterable: false; resizable: false; label: 'common.actions' | t\">\n <span class=\"visually-hidden\">{{ 'common.actions' | t }}</span>\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (col of allColumns(); track col.name; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about this are\n routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom `renderer` does NOT\n suppress this link — a renderer that emits its own <a> produces nested anchors\n (invalid HTML) rather than replacing this one.\n 2. `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 — a Custom.* query that fabricates rows in\n memory, say.\n 3. `rowRoute` replaces the DESTINATION only, never the gate: returning a route\n from it cannot make a row clickable that `canRead()` withheld, and returning\n null from it suppresses the link for that one row. -->\n @if (first && canRead() && rowRouteFor(row); as route) {\n <a [routerLink]=\"route\" [queryParams]=\"rowQueryParams()\" [state]=\"rowLinkState()\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n @if (rowActions().length) {\n <!-- The row menu: every action whose rule accepts one row, run on THIS row only. The\n checkbox selection is left alone (runRowAction never reads or writes it). -->\n <td class=\"spark-row-actions text-end\" (click)=\"$event.stopPropagation()\" (dblclick)=\"$event.stopPropagation()\">\n @if (row) {\n <button type=\"button\" cdkOverlayOrigin #rowMenuOrigin=\"cdkOverlayOrigin\" #rowMenuToggle\n class=\"btn btn-sm btn-link spark-row-menu-toggle\"\n aria-haspopup=\"menu\" [attr.aria-expanded]=\"openRowMenu() === row.id\"\n [attr.aria-label]=\"'common.actions' | t\" (click)=\"toggleRowMenu(row)\">\n <spark-icon name=\"three-dots-vertical\" />\n </button>\n <ng-template cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"rowMenuOrigin\"\n [cdkConnectedOverlayOpen]=\"openRowMenu() === row.id\"\n [cdkConnectedOverlayPositions]=\"rowMenuPositions\"\n (overlayOutsideClick)=\"onRowMenuOutsideClick($event, rowMenuToggle, row.id)\"\n (overlayKeydown)=\"$event.key === 'Escape' && closeRowMenu(row.id)\"\n (detach)=\"closeRowMenu(row.id)\">\n <bs-dropdown-menu class=\"spark-row-menu\">\n @for (action of rowActions(); track action.name) {\n @let unavailable = actionUnavailableReason(action.definition);\n <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\n [class.disabled]=\"unavailable !== null\" [attr.aria-disabled]=\"unavailable !== null ? 'true' : null\"\n [attr.title]=\"unavailable\"\n (click)=\"chooseRowAction(action, row)\">\n {{ action.definition.label | resolveTranslation }}\n </li>\n }\n </bs-dropdown-menu>\n </ng-template>\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n} @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout intact and,\n crucially, makes the failure visible: this branch used to render nothing at all. -->\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n}\n\n<ng-template #cellContent let-item let-col=\"col\">\n <!--\n Presentation lives in <spark-grid-cell>, shared with the AsDetail table on the PO detail page.\n Value resolution stays here: a query row is a PersistentObject with an attribute list, and the\n AsDetail table's rows are plain dictionaries, so only the rendering is common.\n\n `link` is deliberately not passed. The first-column link is the grid's own rule and wraps this\n template from outside; setting both would nest anchors.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(col | queryCellValue:item:lookupReferenceOptions())\"\n [rendererValue]=\"rendererValueFor(item, col)\"\n [item]=\"item\"\n [chips]=\"(col | queryReferenceChips:item)\" />\n</ng-template>\n",
|
|
1442
|
+
template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and `query()` is only\n set after loadData's awaits resolve. Three separate bugs fell out of that one line: the spinner\n was unreachable on a first load (it rendered inside the gate, while `loading` starts true and\n `query` starts null), a first-load failure rendered ZERO DOM — no card, no message, nothing to\n see or report — and a failed RELOAD left the previous query's rows on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the final `@else`\n always renders something. Do not fold these back into one gate, and do not move the spinner\n inside the `query()` branch.\n-->\n@if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n} @else if (query()) {\n <!-- A page-fetch failure while the grid is already resolved. Without this the catch returns an\n empty page and a failed query is indistinguishable from one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <!--\n One datatable, two row sources. `[data]` and `[fetch]` are mutually exclusive in\n ng-bootstrap — binding both makes `[fetch]` win silently — so exactly one is ever non-null\n here: `fetchFn` is left null whenever `data` is bound. A live grid still uses `[fetch]`: its pages\n come from /spark/live instead of /execute (see the class comment).\n -->\n <!--\n The selection bar (#460, M15): the \"N selected\" chip, only while rows are selected, and part\n of the grid rather than of a host, so the query card and the query-list page show the same\n thing. There is deliberately no select-all (owner decision, 2026-09-29): with paged, lazy or\n virtual-scrolled rows it could only tick the rows loaded so far. The selection survives paging\n (#467, D19), so the chip counts every ticked row — exactly what an action receives — and says how\n many are on other pages; its ⊗ drops them all, since the header checkbox only sees this page.\n The bar keeps its height while empty, so ticking the first row does not push the table down\n under the pointer.\n -->\n @if (selectionMode() !== 'none') {\n <div class=\"spark-selection-bar d-flex align-items-center gap-2 mb-2\" style=\"min-height: 1.5rem\">\n @if (selection().length) {\n <bs-badge [type]=\"colors.primary\" class=\"spark-selection-chip\">\n {{ selection().length }} {{ 'common.selected' | t }}\n @if (offPageCount()) {\n <span class=\"spark-selection-off-page\">· {{ offPageCount() }} {{ 'common.onOtherPages' | t }}</span>\n }\n <button type=\"button\" class=\"spark-selection-clear\"\n [attr.aria-label]=\"'common.clearSelection' | t\" (click)=\"clearSelection()\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n </div>\n }\n\n <!-- 'checkbox' (#467, D9): only the checkbox cell selects; a row click opens the row. -->\n <bs-datatable\n [selectionMode]=\"selectionMode() === 'multiple' ? 'checkbox' : 'none'\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n [labels]=\"datatableLabels()\"\n (filterChange)=\"onFilterChange($event)\"\n (rowClick)=\"onRowClick($event)\"\n [(settings)]=\"settings\">\n @for (col of allColumns(); track col.name) {\n <!--\n `canSort`/`canFilter` arrive already resolved by the server (query override → attribute →\n true), so this only spells the polarity. `!== false` is needed for BOTH, for opposite\n reasons: absent means capable on our wire, while the datatable's own `filterable` defaults\n to false, unlike `sortable`.\n\n ⚠️ `filterActive` and `filterSummary` are deliberately NOT bound. Any *bsDatatableColumn\n input change recomputes the column set, which retires the live filter view — while the web\n component's mount-once guard leaves the panel's nodes in place and never re-renders them.\n Binding them to state our own panel mutates would freeze the panel the instant a value is\n ticked, until it is closed and reopened. The cost is that a filtered column carries no\n visual marker yet, which is worth far less than a panel that dies on first use. See #431 F7.\n -->\n <div *bsDatatableColumn=\"col.name; sortable: col.canSort !== false; filterable: col.canFilter !== false; label: (col.label | resolveTranslation) || col.name\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\n\n @if (col.canFilter !== false) {\n <ng-container *bsDatatableFilterPanel=\"let values; let ctx = ctx\">\n <spark-column-filter-panel\n [column]=\"col\"\n [values]=\"values()\"\n [ctx]=\"ctx\"\n [filterLabel]=\"filterLabelFor(col)\" />\n </ng-container>\n }\n </div>\n }\n <!--\n The per-row ⋮ menu's column: no header, never sorted or filtered.\n\n ⚠️ Only once the data columns exist. Those ship WITH the first page, so the rows reach the\n datatable one render before the column templates do. The datatable sizes its columns ONCE,\n from the first render that has both columns and rows, then switches to `table-layout: fixed`\n and never measures again. If this column were already registered it would be the only one\n measured; the data columns would share what it left over equally, and at phone width their\n unclipped headers ran into each other. With no column at all the datatable waits.\n -->\n @if (rowActions().length && allColumns().length) {\n <div *bsDatatableColumn=\"rowActionsColumn; sortable: false; filterable: false; resizable: false; label: 'common.actions' | t\">\n <span class=\"visually-hidden\">{{ 'common.actions' | t }}</span>\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (col of allColumns(); track col.name; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about this are\n routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom `renderer` does NOT\n suppress this link — a renderer that emits its own <a> produces nested anchors\n (invalid HTML) rather than replacing this one.\n 2. `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 — a Custom.* query that fabricates rows in\n memory, say.\n 3. `rowRoute` replaces the DESTINATION only, never the gate: returning a route\n from it cannot make a row clickable that `canRead()` withheld, and returning\n null from it suppresses the link for that one row. -->\n @if (first && canRead() && rowRouteFor(row); as route) {\n <a [routerLink]=\"route\" [queryParams]=\"rowQueryParams()\" [state]=\"rowLinkState()\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n @if (rowActions().length) {\n <!-- The row menu: every action whose rule accepts one row, run on THIS row only. The\n checkbox selection is left alone (runRowAction never reads or writes it). -->\n <td class=\"spark-row-actions text-end\" (click)=\"$event.stopPropagation()\" (dblclick)=\"$event.stopPropagation()\">\n @if (row) {\n <button type=\"button\" cdkOverlayOrigin #rowMenuOrigin=\"cdkOverlayOrigin\" #rowMenuToggle\n class=\"btn btn-sm btn-link spark-row-menu-toggle\"\n aria-haspopup=\"menu\" [attr.aria-expanded]=\"openRowMenu() === row.id\"\n [attr.aria-label]=\"'common.actions' | t\" (click)=\"toggleRowMenu(row)\">\n <spark-icon name=\"three-dots-vertical\" />\n </button>\n <ng-template cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"rowMenuOrigin\"\n [cdkConnectedOverlayOpen]=\"openRowMenu() === row.id\"\n [cdkConnectedOverlayPositions]=\"rowMenuPositions\"\n (overlayOutsideClick)=\"onRowMenuOutsideClick($event, rowMenuToggle, row.id)\"\n (overlayKeydown)=\"$event.key === 'Escape' && closeRowMenu(row.id)\"\n (detach)=\"closeRowMenu(row.id)\">\n <bs-dropdown-menu class=\"spark-row-menu\">\n @for (action of rowActions(); track action.name) {\n @let unavailable = actionUnavailableReason(action.definition);\n <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\n [class.disabled]=\"unavailable !== null\" [attr.aria-disabled]=\"unavailable !== null ? 'true' : null\"\n [attr.title]=\"unavailable\"\n (click)=\"chooseRowAction(action, row)\">\n {{ action.definition.label | resolveTranslation }}\n </li>\n }\n </bs-dropdown-menu>\n </ng-template>\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n} @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout intact and,\n crucially, makes the failure visible: this branch used to render nothing at all. -->\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n}\n\n<ng-template #cellContent let-item let-col=\"col\">\n <!--\n Presentation lives in <spark-grid-cell>, shared with the AsDetail table on the PO detail page.\n Value resolution stays here: a query row is a PersistentObject with an attribute list, and the\n AsDetail table's rows are plain dictionaries, so only the rendering is common.\n\n `link` is deliberately not passed. The first-column link is the grid's own rule and wraps this\n template from outside; setting both would nest anchors.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(col | queryCellValue:item:lookupReferenceOptions())\"\n [rendererValue]=\"rendererValueFor(item, col)\"\n [item]=\"item\"\n [chips]=\"(col | queryReferenceChips:item)\" />\n</ng-template>\n",
|
|
1281
1443
|
styles: [":host ::ng-deep tbody tr[data-selected=true] a:not(.btn),:host ::ng-deep tbody tr[data-selected=true] .spark-row-menu-toggle{color:var(--mp-datatable-row-selected-color)}:host ::ng-deep tbody tr[data-selected=true] .spark-row-menu-toggle{--bs-btn-color: var(--mp-datatable-row-selected-color);--bs-btn-hover-color: var(--mp-datatable-row-selected-color);--bs-btn-active-color: var(--mp-datatable-row-selected-color)}\n"],
|
|
1282
1444
|
dependencies: [
|
|
1283
1445
|
{
|
|
@@ -1539,7 +1701,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1539
1701
|
};
|
|
1540
1702
|
i0.ɵɵngDeclareClassMetadata({
|
|
1541
1703
|
minVersion: "12.0.0",
|
|
1542
|
-
version: "22.2.
|
|
1704
|
+
version: "22.2.2",
|
|
1543
1705
|
ngImport: i0,
|
|
1544
1706
|
type: SparkQueryGridComponent,
|
|
1545
1707
|
decorators: [{
|
|
@@ -1570,7 +1732,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1570
1732
|
SparkIconComponent
|
|
1571
1733
|
],
|
|
1572
1734
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1573
|
-
template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and `query()` is only\n set after loadData's awaits resolve. Three separate bugs fell out of that one line: the spinner\n was unreachable on a first load (it rendered inside the gate, while `loading` starts true and\n `query` starts null), a first-load failure rendered ZERO DOM — no card, no message, nothing to\n see or report — and a failed RELOAD left the previous query's rows on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the final `@else`\n always renders something. Do not fold these back into one gate, and do not move the spinner\n inside the `query()` branch.\n-->\n@if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n} @else if (query()) {\n <!-- A page-fetch failure while the grid is already resolved. Without this the catch returns an\n empty page and a failed query is indistinguishable from one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <!--\n One datatable, two row sources. `[data]` and `[fetch]` are mutually exclusive in\n ng-bootstrap — binding both makes `[fetch]` win silently — so exactly one is ever non-null\n here: `fetchFn` is left null whenever `data` is bound. See the class comment for why rows can\n arrive from outside at all (streaming must not put a websocket in every detail page's bundle).\n -->\n <!--\n The selection bar (#460, M15): the \"N selected\" chip, only while rows are selected, and part\n of the grid rather than of a host, so the query card and the query-list page show the same\n thing. There is deliberately no select-all (owner decision, 2026-09-29): with paged, lazy or\n virtual-scrolled rows it could only tick the rows loaded so far. The selection survives paging\n (#467, D19), so the chip counts every ticked row — exactly what an action receives — and says how\n many are on other pages; its ⊗ drops them all, since the header checkbox only sees this page.\n The bar keeps its height while empty, so ticking the first row does not push the table down\n under the pointer.\n -->\n @if (selectionMode() !== 'none') {\n <div class=\"spark-selection-bar d-flex align-items-center gap-2 mb-2\" style=\"min-height: 1.5rem\">\n @if (selection().length) {\n <bs-badge [type]=\"colors.primary\" class=\"spark-selection-chip\">\n {{ selection().length }} {{ 'common.selected' | t }}\n @if (offPageCount()) {\n <span class=\"spark-selection-off-page\">· {{ offPageCount() }} {{ 'common.onOtherPages' | t }}</span>\n }\n <button type=\"button\" class=\"spark-selection-clear\"\n [attr.aria-label]=\"'common.clearSelection' | t\" (click)=\"clearSelection()\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n </div>\n }\n\n <!-- 'checkbox' (#467, D9): only the checkbox cell selects; a row click opens the row. -->\n <bs-datatable\n [selectionMode]=\"selectionMode() === 'multiple' ? 'checkbox' : 'none'\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n [labels]=\"datatableLabels()\"\n (filterChange)=\"onFilterChange($event)\"\n (rowClick)=\"onRowClick($event)\"\n [(settings)]=\"settings\">\n @for (col of allColumns(); track col.name) {\n <!--\n `canSort`/`canFilter` arrive already resolved by the server (query override → attribute →\n true), so this only spells the polarity. `!== false` is needed for BOTH, for opposite\n reasons: absent means capable on our wire, while the datatable's own `filterable` defaults\n to false, unlike `sortable`.\n\n ⚠️ `filterActive` and `filterSummary` are deliberately NOT bound. Any *bsDatatableColumn\n input change recomputes the column set, which retires the live filter view — while the web\n component's mount-once guard leaves the panel's nodes in place and never re-renders them.\n Binding them to state our own panel mutates would freeze the panel the instant a value is\n ticked, until it is closed and reopened. The cost is that a filtered column carries no\n visual marker yet, which is worth far less than a panel that dies on first use. See #431 F7.\n -->\n <div *bsDatatableColumn=\"col.name; sortable: col.canSort !== false; filterable: col.canFilter !== false; label: (col.label | resolveTranslation) || col.name\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\n\n @if (col.canFilter !== false) {\n <ng-container *bsDatatableFilterPanel=\"let values; let ctx = ctx\">\n <spark-column-filter-panel\n [column]=\"col\"\n [values]=\"values()\"\n [ctx]=\"ctx\"\n [filterLabel]=\"filterLabelFor(col)\" />\n </ng-container>\n }\n </div>\n }\n <!--\n The per-row ⋮ menu's column: no header, never sorted or filtered.\n\n ⚠️ Only once the data columns exist. Those ship WITH the first page, so the rows reach the\n datatable one render before the column templates do. The datatable sizes its columns ONCE,\n from the first render that has both columns and rows, then switches to `table-layout: fixed`\n and never measures again. If this column were already registered it would be the only one\n measured; the data columns would share what it left over equally, and at phone width their\n unclipped headers ran into each other. With no column at all the datatable waits.\n -->\n @if (rowActions().length && allColumns().length) {\n <div *bsDatatableColumn=\"rowActionsColumn; sortable: false; filterable: false; resizable: false; label: 'common.actions' | t\">\n <span class=\"visually-hidden\">{{ 'common.actions' | t }}</span>\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (col of allColumns(); track col.name; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about this are\n routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom `renderer` does NOT\n suppress this link — a renderer that emits its own <a> produces nested anchors\n (invalid HTML) rather than replacing this one.\n 2. `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 — a Custom.* query that fabricates rows in\n memory, say.\n 3. `rowRoute` replaces the DESTINATION only, never the gate: returning a route\n from it cannot make a row clickable that `canRead()` withheld, and returning\n null from it suppresses the link for that one row. -->\n @if (first && canRead() && rowRouteFor(row); as route) {\n <a [routerLink]=\"route\" [queryParams]=\"rowQueryParams()\" [state]=\"rowLinkState()\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n @if (rowActions().length) {\n <!-- The row menu: every action whose rule accepts one row, run on THIS row only. The\n checkbox selection is left alone (runRowAction never reads or writes it). -->\n <td class=\"spark-row-actions text-end\" (click)=\"$event.stopPropagation()\" (dblclick)=\"$event.stopPropagation()\">\n @if (row) {\n <button type=\"button\" cdkOverlayOrigin #rowMenuOrigin=\"cdkOverlayOrigin\" #rowMenuToggle\n class=\"btn btn-sm btn-link spark-row-menu-toggle\"\n aria-haspopup=\"menu\" [attr.aria-expanded]=\"openRowMenu() === row.id\"\n [attr.aria-label]=\"'common.actions' | t\" (click)=\"toggleRowMenu(row)\">\n <spark-icon name=\"three-dots-vertical\" />\n </button>\n <ng-template cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"rowMenuOrigin\"\n [cdkConnectedOverlayOpen]=\"openRowMenu() === row.id\"\n [cdkConnectedOverlayPositions]=\"rowMenuPositions\"\n (overlayOutsideClick)=\"onRowMenuOutsideClick($event, rowMenuToggle, row.id)\"\n (overlayKeydown)=\"$event.key === 'Escape' && closeRowMenu(row.id)\"\n (detach)=\"closeRowMenu(row.id)\">\n <bs-dropdown-menu class=\"spark-row-menu\">\n @for (action of rowActions(); track action.name) {\n @let unavailable = actionUnavailableReason(action.definition);\n <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\n [class.disabled]=\"unavailable !== null\" [attr.aria-disabled]=\"unavailable !== null ? 'true' : null\"\n [attr.title]=\"unavailable\"\n (click)=\"chooseRowAction(action, row)\">\n {{ action.definition.label | resolveTranslation }}\n </li>\n }\n </bs-dropdown-menu>\n </ng-template>\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n} @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout intact and,\n crucially, makes the failure visible: this branch used to render nothing at all. -->\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n}\n\n<ng-template #cellContent let-item let-col=\"col\">\n <!--\n Presentation lives in <spark-grid-cell>, shared with the AsDetail table on the PO detail page.\n Value resolution stays here: a query row is a PersistentObject with an attribute list, and the\n AsDetail table's rows are plain dictionaries, so only the rendering is common.\n\n `link` is deliberately not passed. The first-column link is the grid's own rule and wraps this\n template from outside; setting both would nest anchors.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(col | queryCellValue:item:lookupReferenceOptions())\"\n [rendererValue]=\"rendererValueFor(item, col)\"\n [item]=\"item\"\n [chips]=\"(col | queryReferenceChips:item)\" />\n</ng-template>\n",
|
|
1735
|
+
template: "<!--\n Three explicit states, in this order, and the order is load-bearing.\n\n This template used to open with `@if (query(); as q)` wrapping EVERYTHING, and `query()` is only\n set after loadData's awaits resolve. Three separate bugs fell out of that one line: the spinner\n was unreachable on a first load (it rendered inside the gate, while `loading` starts true and\n `query` starts null), a first-load failure rendered ZERO DOM — no card, no message, nothing to\n see or report — and a failed RELOAD left the previous query's rows on screen.\n\n So: `loading` is checked first and outside any dependency on `query`, and the final `@else`\n always renders something. Do not fold these back into one gate, and do not move the spinner\n inside the `query()` branch.\n-->\n@if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n} @else if (query()) {\n <!-- A page-fetch failure while the grid is already resolved. Without this the catch returns an\n empty page and a failed query is indistinguishable from one that legitimately has no rows. -->\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <!--\n One datatable, two row sources. `[data]` and `[fetch]` are mutually exclusive in\n ng-bootstrap — binding both makes `[fetch]` win silently — so exactly one is ever non-null\n here: `fetchFn` is left null whenever `data` is bound. A live grid still uses `[fetch]`: its pages\n come from /spark/live instead of /execute (see the class comment).\n -->\n <!--\n The selection bar (#460, M15): the \"N selected\" chip, only while rows are selected, and part\n of the grid rather than of a host, so the query card and the query-list page show the same\n thing. There is deliberately no select-all (owner decision, 2026-09-29): with paged, lazy or\n virtual-scrolled rows it could only tick the rows loaded so far. The selection survives paging\n (#467, D19), so the chip counts every ticked row — exactly what an action receives — and says how\n many are on other pages; its ⊗ drops them all, since the header checkbox only sees this page.\n The bar keeps its height while empty, so ticking the first row does not push the table down\n under the pointer.\n -->\n @if (selectionMode() !== 'none') {\n <div class=\"spark-selection-bar d-flex align-items-center gap-2 mb-2\" style=\"min-height: 1.5rem\">\n @if (selection().length) {\n <bs-badge [type]=\"colors.primary\" class=\"spark-selection-chip\">\n {{ selection().length }} {{ 'common.selected' | t }}\n @if (offPageCount()) {\n <span class=\"spark-selection-off-page\">· {{ offPageCount() }} {{ 'common.onOtherPages' | t }}</span>\n }\n <button type=\"button\" class=\"spark-selection-clear\"\n [attr.aria-label]=\"'common.clearSelection' | t\" (click)=\"clearSelection()\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n </div>\n }\n\n <!-- 'checkbox' (#467, D9): only the checkbox cell selects; a row click opens the row. -->\n <bs-datatable\n [selectionMode]=\"selectionMode() === 'multiple' ? 'checkbox' : 'none'\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n [labels]=\"datatableLabels()\"\n (filterChange)=\"onFilterChange($event)\"\n (rowClick)=\"onRowClick($event)\"\n [(settings)]=\"settings\">\n @for (col of allColumns(); track col.name) {\n <!--\n `canSort`/`canFilter` arrive already resolved by the server (query override → attribute →\n true), so this only spells the polarity. `!== false` is needed for BOTH, for opposite\n reasons: absent means capable on our wire, while the datatable's own `filterable` defaults\n to false, unlike `sortable`.\n\n ⚠️ `filterActive` and `filterSummary` are deliberately NOT bound. Any *bsDatatableColumn\n input change recomputes the column set, which retires the live filter view — while the web\n component's mount-once guard leaves the panel's nodes in place and never re-renders them.\n Binding them to state our own panel mutates would freeze the panel the instant a value is\n ticked, until it is closed and reopened. The cost is that a filtered column carries no\n visual marker yet, which is worth far less than a panel that dies on first use. See #431 F7.\n -->\n <div *bsDatatableColumn=\"col.name; sortable: col.canSort !== false; filterable: col.canFilter !== false; label: (col.label | resolveTranslation) || col.name\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\n\n @if (col.canFilter !== false) {\n <ng-container *bsDatatableFilterPanel=\"let values; let ctx = ctx\">\n <spark-column-filter-panel\n [column]=\"col\"\n [values]=\"values()\"\n [ctx]=\"ctx\"\n [filterLabel]=\"filterLabelFor(col)\" />\n </ng-container>\n }\n </div>\n }\n <!--\n The per-row ⋮ menu's column: no header, never sorted or filtered.\n\n ⚠️ Only once the data columns exist. Those ship WITH the first page, so the rows reach the\n datatable one render before the column templates do. The datatable sizes its columns ONCE,\n from the first render that has both columns and rows, then switches to `table-layout: fixed`\n and never measures again. If this column were already registered it would be the only one\n measured; the data columns would share what it left over equally, and at phone width their\n unclipped headers ran into each other. With no column at all the datatable waits.\n -->\n @if (rowActions().length && allColumns().length) {\n <div *bsDatatableColumn=\"rowActionsColumn; sortable: false; filterable: false; resizable: false; label: 'common.actions' | t\">\n <span class=\"visually-hidden\">{{ 'common.actions' | t }}</span>\n </div>\n }\n\n <ng-container *bsRowTemplate=\"let item\">\n @let row = $any(item);\n @for (col of allColumns(); track col.name; let first = $first) {\n <td>\n @if (row) {\n <!-- The first column links to the row's detail page. Two things about this are\n routinely misread:\n\n 1. `cellContent` is projected INSIDE the anchor, so a custom `renderer` does NOT\n suppress this link — a renderer that emits its own <a> produces nested anchors\n (invalid HTML) rather than replacing this one.\n 2. `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 — a Custom.* query that fabricates rows in\n memory, say.\n 3. `rowRoute` replaces the DESTINATION only, never the gate: returning a route\n from it cannot make a row clickable that `canRead()` withheld, and returning\n null from it suppresses the link for that one row. -->\n @if (first && canRead() && rowRouteFor(row); as route) {\n <a [routerLink]=\"route\" [queryParams]=\"rowQueryParams()\" [state]=\"rowLinkState()\"\n (click)=\"rowClicked.emit(row)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: row, col: col }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n @if (rowActions().length) {\n <!-- The row menu: every action whose rule accepts one row, run on THIS row only. The\n checkbox selection is left alone (runRowAction never reads or writes it). -->\n <td class=\"spark-row-actions text-end\" (click)=\"$event.stopPropagation()\" (dblclick)=\"$event.stopPropagation()\">\n @if (row) {\n <button type=\"button\" cdkOverlayOrigin #rowMenuOrigin=\"cdkOverlayOrigin\" #rowMenuToggle\n class=\"btn btn-sm btn-link spark-row-menu-toggle\"\n aria-haspopup=\"menu\" [attr.aria-expanded]=\"openRowMenu() === row.id\"\n [attr.aria-label]=\"'common.actions' | t\" (click)=\"toggleRowMenu(row)\">\n <spark-icon name=\"three-dots-vertical\" />\n </button>\n <ng-template cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"rowMenuOrigin\"\n [cdkConnectedOverlayOpen]=\"openRowMenu() === row.id\"\n [cdkConnectedOverlayPositions]=\"rowMenuPositions\"\n (overlayOutsideClick)=\"onRowMenuOutsideClick($event, rowMenuToggle, row.id)\"\n (overlayKeydown)=\"$event.key === 'Escape' && closeRowMenu(row.id)\"\n (detach)=\"closeRowMenu(row.id)\">\n <bs-dropdown-menu class=\"spark-row-menu\">\n @for (action of rowActions(); track action.name) {\n @let unavailable = actionUnavailableReason(action.definition);\n <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\n [class.disabled]=\"unavailable !== null\" [attr.aria-disabled]=\"unavailable !== null ? 'true' : null\"\n [attr.title]=\"unavailable\"\n (click)=\"chooseRowAction(action, row)\">\n {{ action.definition.label | resolveTranslation }}\n </li>\n }\n </bs-dropdown-menu>\n </ng-template>\n }\n </td>\n }\n </ng-container>\n </bs-datatable>\n} @else {\n <!-- The query could not be resolved. Rendering something keeps the host's layout intact and,\n crucially, makes the failure visible: this branch used to render nothing at all. -->\n <bs-alert [type]=\"colors.danger\" class=\"mb-0\">\n {{ errorMessage() || ('spark.query.unavailable' | t) }}\n </bs-alert>\n}\n\n<ng-template #cellContent let-item let-col=\"col\">\n <!--\n Presentation lives in <spark-grid-cell>, shared with the AsDetail table on the PO detail page.\n Value resolution stays here: a query row is a PersistentObject with an attribute list, and the\n AsDetail table's rows are plain dictionaries, so only the rendering is common.\n\n `link` is deliberately not passed. The first-column link is the grid's own rule and wraps this\n template from outside; setting both would nest anchors.\n -->\n <spark-grid-cell\n [column]=\"col\"\n [display]=\"(col | queryCellValue:item:lookupReferenceOptions())\"\n [rendererValue]=\"rendererValueFor(item, col)\"\n [item]=\"item\"\n [chips]=\"(col | queryReferenceChips:item)\" />\n</ng-template>\n",
|
|
1574
1736
|
styles: [":host ::ng-deep tbody tr[data-selected=true] a:not(.btn),:host ::ng-deep tbody tr[data-selected=true] .spark-row-menu-toggle{color:var(--mp-datatable-row-selected-color)}:host ::ng-deep tbody tr[data-selected=true] .spark-row-menu-toggle{--bs-btn-color: var(--mp-datatable-row-selected-color);--bs-btn-hover-color: var(--mp-datatable-row-selected-color);--bs-btn-active-color: var(--mp-datatable-row-selected-color)}\n"]
|
|
1575
1737
|
}]
|
|
1576
1738
|
}],
|
|
@@ -1616,6 +1778,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1616
1778
|
required: false
|
|
1617
1779
|
}]
|
|
1618
1780
|
}],
|
|
1781
|
+
live: [{
|
|
1782
|
+
type: i0.Input,
|
|
1783
|
+
args: [{
|
|
1784
|
+
isSignal: true,
|
|
1785
|
+
alias: "live",
|
|
1786
|
+
required: false
|
|
1787
|
+
}]
|
|
1788
|
+
}],
|
|
1619
1789
|
search: [{
|
|
1620
1790
|
type: i0.Input,
|
|
1621
1791
|
args: [{
|
|
@@ -1790,7 +1960,7 @@ var SparkSearchBoxComponent = class SparkSearchBoxComponent {
|
|
|
1790
1960
|
}
|
|
1791
1961
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1792
1962
|
minVersion: "12.0.0",
|
|
1793
|
-
version: "22.2.
|
|
1963
|
+
version: "22.2.2",
|
|
1794
1964
|
ngImport: i0,
|
|
1795
1965
|
type: SparkSearchBoxComponent,
|
|
1796
1966
|
deps: [],
|
|
@@ -1798,7 +1968,7 @@ var SparkSearchBoxComponent = class SparkSearchBoxComponent {
|
|
|
1798
1968
|
});
|
|
1799
1969
|
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1800
1970
|
minVersion: "17.0.0",
|
|
1801
|
-
version: "22.2.
|
|
1971
|
+
version: "22.2.2",
|
|
1802
1972
|
type: SparkSearchBoxComponent,
|
|
1803
1973
|
isStandalone: true,
|
|
1804
1974
|
selector: "spark-search-box",
|
|
@@ -1918,7 +2088,7 @@ var SparkSearchBoxComponent = class SparkSearchBoxComponent {
|
|
|
1918
2088
|
};
|
|
1919
2089
|
i0.ɵɵngDeclareClassMetadata({
|
|
1920
2090
|
minVersion: "12.0.0",
|
|
1921
|
-
version: "22.2.
|
|
2091
|
+
version: "22.2.2",
|
|
1922
2092
|
ngImport: i0,
|
|
1923
2093
|
type: SparkSearchBoxComponent,
|
|
1924
2094
|
decorators: [{
|
|
@@ -2045,7 +2215,7 @@ var SparkQueryCardComponent = class SparkQueryCardComponent {
|
|
|
2045
2215
|
selection = computed(() => this.grid()?.selection() ?? [], ...ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []);
|
|
2046
2216
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
2047
2217
|
minVersion: "12.0.0",
|
|
2048
|
-
version: "22.2.
|
|
2218
|
+
version: "22.2.2",
|
|
2049
2219
|
ngImport: i0,
|
|
2050
2220
|
type: SparkQueryCardComponent,
|
|
2051
2221
|
deps: [],
|
|
@@ -2053,7 +2223,7 @@ var SparkQueryCardComponent = class SparkQueryCardComponent {
|
|
|
2053
2223
|
});
|
|
2054
2224
|
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2055
2225
|
minVersion: "17.0.0",
|
|
2056
|
-
version: "22.2.
|
|
2226
|
+
version: "22.2.2",
|
|
2057
2227
|
type: SparkQueryCardComponent,
|
|
2058
2228
|
isStandalone: true,
|
|
2059
2229
|
selector: "spark-query-card",
|
|
@@ -2246,6 +2416,7 @@ var SparkQueryCardComponent = class SparkQueryCardComponent {
|
|
|
2246
2416
|
"parentType",
|
|
2247
2417
|
"presetFilters",
|
|
2248
2418
|
"data",
|
|
2419
|
+
"live",
|
|
2249
2420
|
"search",
|
|
2250
2421
|
"columns",
|
|
2251
2422
|
"reloadToken",
|
|
@@ -2293,7 +2464,7 @@ var SparkQueryCardComponent = class SparkQueryCardComponent {
|
|
|
2293
2464
|
};
|
|
2294
2465
|
i0.ɵɵngDeclareClassMetadata({
|
|
2295
2466
|
minVersion: "12.0.0",
|
|
2296
|
-
version: "22.2.
|
|
2467
|
+
version: "22.2.2",
|
|
2297
2468
|
ngImport: i0,
|
|
2298
2469
|
type: SparkQueryCardComponent,
|
|
2299
2470
|
decorators: [{
|