@mintplayer/ng-spark 22.26.0 → 22.28.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 +6 -4
- package/fesm2022/mintplayer-ng-spark-grid.mjs +73 -50
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +19 -10
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +5 -2
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-create.mjs +16 -7
- package/fesm2022/mintplayer-ng-spark-po-create.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +30 -14
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +45 -15
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +4 -4
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +18 -2
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +2 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +0 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +12 -9
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs +4 -3
- package/fesm2022/mintplayer-ng-spark-soft-delete.mjs.map +1 -1
- package/package.json +3 -3
- package/types/mintplayer-ng-spark-grid.d.ts +50 -46
- package/types/mintplayer-ng-spark-models.d.ts +75 -40
- package/types/mintplayer-ng-spark-moderation.d.ts +8 -4
- package/types/mintplayer-ng-spark-pipes.d.ts +3 -0
- package/types/mintplayer-ng-spark-po-create.d.ts +4 -0
- package/types/mintplayer-ng-spark-po-detail.d.ts +12 -2
- package/types/mintplayer-ng-spark-po-edit.d.ts +16 -0
- package/types/mintplayer-ng-spark-po-form.d.ts +1 -0
- package/types/mintplayer-ng-spark-query-list.d.ts +11 -1
- package/types/mintplayer-ng-spark-services.d.ts +38 -9
- package/types/mintplayer-ng-spark-soft-delete.d.ts +1 -1
package/README.md
CHANGED
|
@@ -118,12 +118,14 @@ The grid also renders:
|
|
|
118
118
|
[ng-bootstrap #419](https://github.com/MintPlayer/mintplayer-ng-bootstrap/issues/419) (the
|
|
119
119
|
dropdown fesm fails to initialise in unlinked JIT test runs).
|
|
120
120
|
|
|
121
|
-
- **Selection** follows `selectionMode` (`'auto' | 'none' | '
|
|
121
|
+
- **Selection** follows `selectionMode` (`'auto' | 'none' | 'multiple'`).
|
|
122
122
|
- It is set on the query, or on the parent type's `queries` entry, which `<spark-query-card
|
|
123
123
|
[selectionMode]>` and `<spark-query-grid [selectionModeSetting]>` take.
|
|
124
|
-
- `'auto'`
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
- `'auto'` shows checkboxes when an action the user may run needs a selection: Edit, Delete or a
|
|
125
|
+
custom action (#467).
|
|
126
|
+
- **Delete** posts the selection to `SparkService.deleteMany()` (`/spark/po/delete-many`), each row
|
|
127
|
+
with the `etag` its query row carried (#467, D14). One request deletes all the rows or none; a row
|
|
128
|
+
changed since the list loaded is a 409 that names it.
|
|
127
129
|
- **New** on a sub-query navigates to the create page with `parentId`, `parentType` and `queryId`.
|
|
128
130
|
The create page always asks `SparkService.newObject()` (`/spark/po/new`) for its blank object, so
|
|
129
131
|
the server's `OnNewAsync` sets the defaults. For a sub-query, its base fills the reference to the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BsDatatableColumnDirective, BsDatatableComponent, BsDatatableFilterPanelDirective, BsRowTemplateDirective, DatatableSettings } from "@mintplayer/ng-bootstrap/datatable";
|
|
2
|
-
import { ShowedOn, defaultQueryActions, filterQueryActions, hasShowedOnFlag, parseSelectionRule, selectionModeFor, valueFor } from "@mintplayer/ng-spark/models";
|
|
3
2
|
import * as i0 from "@angular/core";
|
|
4
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
|
+
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
6
|
import { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
|
|
7
7
|
import * as i1$1 from "@angular/common";
|
|
@@ -33,9 +33,6 @@ const SPARK_GRID_PAGE_SIZES = [
|
|
|
33
33
|
25,
|
|
34
34
|
50
|
|
35
35
|
];
|
|
36
|
-
function visibleGridAttributes(entityType) {
|
|
37
|
-
return entityType?.attributes.filter((a) => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.Query)).sort((a, b) => a.order - b.order) ?? [];
|
|
38
|
-
}
|
|
39
36
|
function initialGridSettings(query) {
|
|
40
37
|
const sortColumns = (query?.sortColumns || []).map((sc) => ({
|
|
41
38
|
property: sc.property,
|
|
@@ -297,7 +294,7 @@ function applicablePresetFilters(filters, entityType) {
|
|
|
297
294
|
const result = [];
|
|
298
295
|
for (const filter of filters) {
|
|
299
296
|
const attribute = (entityType.attributes ?? []).find((a) => a.name === filter.name);
|
|
300
|
-
if (!attribute || attribute.
|
|
297
|
+
if (!attribute || !hasShowedOnFlag(attribute.showedOn, ShowedOn.Query)) continue;
|
|
301
298
|
const coerce = (value) => {
|
|
302
299
|
if (typeof value !== "string") return value;
|
|
303
300
|
if (NUMERIC_TYPES.has(attribute.dataType)) {
|
|
@@ -549,15 +546,9 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
549
546
|
currentUrl = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map(() => this.router.url)), { initialValue: this.router.url });
|
|
550
547
|
rowLinkState = computed(() => ({ [SPARK_RETURN_URL_STATE_KEY]: this.currentUrl() }), ...ngDevMode ? [{ debugName: "rowLinkState" }] : /* istanbul ignore next */ []);
|
|
551
548
|
onRowClick(event) {
|
|
552
|
-
if (this.selectionMode() === "none") this.openRow(event);
|
|
553
|
-
}
|
|
554
|
-
onRowDblClick(event) {
|
|
555
|
-
if (this.selectionMode() !== "none") this.openRow(event);
|
|
556
|
-
}
|
|
557
|
-
openRow(event) {
|
|
558
549
|
const row = event.row;
|
|
559
550
|
const original = event.originalEvent;
|
|
560
|
-
if ((original?.target)?.closest?.("a, button, input, select, textarea, label, [role=\"button\"]")) return;
|
|
551
|
+
if ((original?.target)?.closest?.("a, button, input, select, textarea, label, [role=\"button\"], .checkbox-cell")) return;
|
|
561
552
|
if (original && (original.ctrlKey || original.metaKey || original.shiftKey || original.button === 1)) return;
|
|
562
553
|
if (!row || !this.canRead()) return;
|
|
563
554
|
const route = this.rowRouteFor(row);
|
|
@@ -624,10 +615,10 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
624
615
|
}, ...ngDevMode ? [{ debugName: "visibleCustomActions" }] : /* istanbul ignore next */ []);
|
|
625
616
|
fetchFn = signal(null, ...ngDevMode ? [{ debugName: "fetchFn" }] : /* istanbul ignore next */ []);
|
|
626
617
|
errorMessage = signal(null, ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const actions = [];
|
|
618
|
+
offeredDefaultActions = computed(() => {
|
|
619
|
+
if (this.recycleBin()) return [];
|
|
630
620
|
const withheld = new Set(this.disabledActions().map((name) => name.toLowerCase()));
|
|
621
|
+
const actions = [];
|
|
631
622
|
for (const definition of this.defaultActions()) {
|
|
632
623
|
const name = definition.name.toLowerCase();
|
|
633
624
|
if (name === "new" && this.offersCreate()) actions.push({
|
|
@@ -636,13 +627,25 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
636
627
|
definition,
|
|
637
628
|
priority: 1 + definition.offset
|
|
638
629
|
});
|
|
639
|
-
else if (name === "
|
|
630
|
+
else if (name === "edit" && !withheld.has("edit") && !withheld.has("save")) actions.push({
|
|
631
|
+
kind: "edit",
|
|
632
|
+
name: definition.name,
|
|
633
|
+
definition,
|
|
634
|
+
priority: 3 + definition.offset
|
|
635
|
+
});
|
|
636
|
+
else if (name === "delete" && !withheld.has("delete")) actions.push({
|
|
640
637
|
kind: "delete",
|
|
641
638
|
name: definition.name,
|
|
642
639
|
definition,
|
|
643
640
|
priority: 5 + definition.offset
|
|
644
641
|
});
|
|
645
642
|
}
|
|
643
|
+
return actions;
|
|
644
|
+
}, ...ngDevMode ? [{ debugName: "offeredDefaultActions" }] : /* istanbul ignore next */ []);
|
|
645
|
+
selectionMode = computed(() => selectionModeFor([...this.offeredDefaultActions().map((a) => a.definition), ...this.visibleCustomActions()], this.selectionModeSetting() ?? this.query()?.selectionMode ?? "auto"), ...ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []);
|
|
646
|
+
toolbarActions = computed(() => {
|
|
647
|
+
const selectable = this.selectionMode() !== "none";
|
|
648
|
+
const actions = this.offeredDefaultActions().filter((a) => a.kind === "new" || selectable);
|
|
646
649
|
for (const definition of this.visibleCustomActions()) actions.push({
|
|
647
650
|
kind: "custom",
|
|
648
651
|
name: definition.name,
|
|
@@ -652,15 +655,8 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
652
655
|
return actions;
|
|
653
656
|
}, ...ngDevMode ? [{ debugName: "toolbarActions" }] : /* istanbul ignore next */ []);
|
|
654
657
|
rowActions = computed(() => {
|
|
655
|
-
const withheld = new Set(this.disabledActions().map((name) => name.toLowerCase()));
|
|
656
658
|
const rowTaking = (definition) => !!definition.selectionRule?.trim() && parseSelectionRule(definition.selectionRule)(1);
|
|
657
|
-
const actions =
|
|
658
|
-
for (const definition of this.defaultActions()) if (definition.name.toLowerCase() === "delete" && this.offersDelete(withheld) && rowTaking(definition)) actions.push({
|
|
659
|
-
kind: "delete",
|
|
660
|
-
name: definition.name,
|
|
661
|
-
definition,
|
|
662
|
-
priority: 5 + definition.offset
|
|
663
|
-
});
|
|
659
|
+
const actions = this.offeredDefaultActions().filter((a) => a.kind !== "new" && rowTaking(a.definition));
|
|
664
660
|
for (const definition of this.visibleCustomActions()) if (rowTaking(definition)) actions.push({
|
|
665
661
|
kind: "custom",
|
|
666
662
|
name: definition.name,
|
|
@@ -678,7 +674,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
678
674
|
priority,
|
|
679
675
|
definition: {
|
|
680
676
|
name: addon.id,
|
|
681
|
-
|
|
677
|
+
label: { en: this.lang.t(addon.labelKey) },
|
|
682
678
|
showedOn: "query",
|
|
683
679
|
refreshOnCompleted: true,
|
|
684
680
|
offset: priority
|
|
@@ -698,9 +694,6 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
698
694
|
deleted: this.effectiveDeleted() ?? null
|
|
699
695
|
};
|
|
700
696
|
}, ...ngDevMode ? [{ debugName: "rowActionScope" }] : /* istanbul ignore next */ []);
|
|
701
|
-
offersDelete(withheld) {
|
|
702
|
-
return !this.recycleBin() && !withheld.has("delete");
|
|
703
|
-
}
|
|
704
697
|
isToolbarActionEnabled(action) {
|
|
705
698
|
return this.isActionEnabled(action.definition);
|
|
706
699
|
}
|
|
@@ -709,15 +702,33 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
709
702
|
case "new":
|
|
710
703
|
this.startNew();
|
|
711
704
|
return;
|
|
705
|
+
case "edit":
|
|
706
|
+
this.editRow(this.selection()[0]);
|
|
707
|
+
return;
|
|
712
708
|
case "delete":
|
|
713
|
-
await this.deleteRows(this.selection()
|
|
709
|
+
await this.deleteRows(this.selection(), action.definition);
|
|
714
710
|
return;
|
|
715
711
|
default: await this.onCustomAction(action.definition);
|
|
716
712
|
}
|
|
717
713
|
}
|
|
714
|
+
editRow(row) {
|
|
715
|
+
if (!row) return;
|
|
716
|
+
const route = this.rowRouteFor(row);
|
|
717
|
+
if (!route) return;
|
|
718
|
+
this.router.navigate([...route, "edit"], {
|
|
719
|
+
queryParams: this.rowQueryParams() ?? void 0,
|
|
720
|
+
state: this.rowLinkState()
|
|
721
|
+
});
|
|
722
|
+
}
|
|
718
723
|
clearSelection() {
|
|
719
724
|
this.selection.set([]);
|
|
720
725
|
}
|
|
726
|
+
loadedIds = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "loadedIds" }] : /* istanbul ignore next */ []);
|
|
727
|
+
offPageCount = computed(() => {
|
|
728
|
+
const hostRows = this.data();
|
|
729
|
+
const loaded = hostRows ? new Set(hostRows.map((r) => r.id)) : this.loadedIds();
|
|
730
|
+
return this.selection().filter((r) => !loaded.has(r.id)).length;
|
|
731
|
+
}, ...ngDevMode ? [{ debugName: "offPageCount" }] : /* istanbul ignore next */ []);
|
|
721
732
|
openRowMenu = signal(null, ...ngDevMode ? [{ debugName: "openRowMenu" }] : /* istanbul ignore next */ []);
|
|
722
733
|
rowMenuPositions = [{
|
|
723
734
|
originX: "end",
|
|
@@ -746,8 +757,12 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
746
757
|
await this.runRowAction(action, row);
|
|
747
758
|
}
|
|
748
759
|
async runRowAction(action, row) {
|
|
760
|
+
if (action.kind === "edit") {
|
|
761
|
+
this.editRow(row);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
749
764
|
if (action.kind === "delete") {
|
|
750
|
-
await this.deleteRows([row
|
|
765
|
+
await this.deleteRows([row], action.definition);
|
|
751
766
|
return;
|
|
752
767
|
}
|
|
753
768
|
if (action.kind === "addon" && action.addon) {
|
|
@@ -794,17 +809,22 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
794
809
|
state: this.rowLinkState()
|
|
795
810
|
});
|
|
796
811
|
}
|
|
797
|
-
async deleteRows(
|
|
812
|
+
async deleteRows(rows, definition) {
|
|
798
813
|
const type = this.entityType();
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
814
|
+
const queryId = this.query()?.id;
|
|
815
|
+
const items = rows.filter((r) => !!r.etag).map((r) => ({
|
|
816
|
+
id: r.id,
|
|
817
|
+
etag: r.etag
|
|
818
|
+
}));
|
|
819
|
+
if (!type || !queryId || !items.length || items.length !== rows.length) return;
|
|
820
|
+
const ids = items.map((r) => r.id);
|
|
821
|
+
const message = confirmationText(definition, ids.length);
|
|
822
|
+
if (message && !confirm(message)) return;
|
|
803
823
|
const parentId = this.parentId();
|
|
804
824
|
const parentType = this.parentType();
|
|
805
825
|
try {
|
|
806
|
-
await this.sparkService.deleteMany(type.id,
|
|
807
|
-
queryId
|
|
826
|
+
await this.sparkService.deleteMany(type.id, items, {
|
|
827
|
+
queryId,
|
|
808
828
|
...parentId && parentType ? {
|
|
809
829
|
parentId,
|
|
810
830
|
parentType
|
|
@@ -815,13 +835,12 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
815
835
|
this.reload();
|
|
816
836
|
} catch (e) {
|
|
817
837
|
const err = e;
|
|
818
|
-
this.errorMessage.set(err.error?.result?.errors?.[0]?.message || err.error?.result?.error || err.error?.error || (err.status === 404 ? this.lang.t("common.deleteRefused") : "") || err.message || this.lang.t("common.actionFailed") || "Action failed");
|
|
838
|
+
this.errorMessage.set(err.error?.result?.errors?.[0]?.message || (err.status === 409 ? err.error?.result?.message || this.lang.t("common.concurrencyConflict") : "") || err.error?.result?.error || err.error?.error || (err.status === 404 ? this.lang.t("common.deleteRefused") : "") || err.message || this.lang.t("common.actionFailed") || "Action failed");
|
|
819
839
|
}
|
|
820
840
|
}
|
|
821
841
|
isVirtualScrolling = computed(() => isVirtualScrollingQuery(this.query()), ...ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []);
|
|
822
842
|
fetchedColumns = signal([], ...ngDevMode ? [{ debugName: "fetchedColumns" }] : /* istanbul ignore next */ []);
|
|
823
843
|
allColumns = computed(() => this.columns() ?? this.fetchedColumns(), ...ngDevMode ? [{ debugName: "allColumns" }] : /* istanbul ignore next */ []);
|
|
824
|
-
visibleColumns = computed(() => this.allColumns().filter((c) => c.isVisible !== false), ...ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []);
|
|
825
844
|
hasExternalData = computed(() => this.data() !== null || this.query()?.isStreamingQuery === true, ...ngDevMode ? [{ debugName: "hasExternalData" }] : /* istanbul ignore next */ []);
|
|
826
845
|
isActionEnabled(action) {
|
|
827
846
|
return parseSelectionRule(action.selectionRule)(this.selection().map((r) => r.id).length);
|
|
@@ -873,6 +892,9 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
873
892
|
this.onFilterChanged();
|
|
874
893
|
});
|
|
875
894
|
});
|
|
895
|
+
effect(() => {
|
|
896
|
+
if (this.selectionMode() === "none" && untracked(() => this.selection().length)) untracked(() => this.selection.set([]));
|
|
897
|
+
});
|
|
876
898
|
let firstDeleted = true;
|
|
877
899
|
effect(() => {
|
|
878
900
|
this.effectiveDeleted();
|
|
@@ -955,6 +977,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
955
977
|
});
|
|
956
978
|
}
|
|
957
979
|
this.filters.set(next);
|
|
980
|
+
this.selection.set([]);
|
|
958
981
|
this.onFilterChanged();
|
|
959
982
|
}
|
|
960
983
|
onFilterChanged() {
|
|
@@ -974,10 +997,8 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
974
997
|
this.reload();
|
|
975
998
|
}
|
|
976
999
|
async onCustomAction(action, rowIds) {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
if (!confirm(message)) return;
|
|
980
|
-
}
|
|
1000
|
+
const message = confirmationText(action, rowIds?.length ?? this.selection().length);
|
|
1001
|
+
if (message && !confirm(message)) return;
|
|
981
1002
|
try {
|
|
982
1003
|
const pId = this.parentId();
|
|
983
1004
|
const pType = this.parentType();
|
|
@@ -1065,6 +1086,8 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1065
1086
|
this.resultCount.set(r.totalItems);
|
|
1066
1087
|
this.setFetchedColumns(r.columns ?? []);
|
|
1067
1088
|
this.disabledActions.set(r.disabledActions ?? []);
|
|
1089
|
+
const ids = r.items.map((item) => item.id);
|
|
1090
|
+
this.loadedIds.set(this.isVirtualScrolling() ? /* @__PURE__ */ new Set([...this.loadedIds(), ...ids]) : new Set(ids));
|
|
1068
1091
|
return {
|
|
1069
1092
|
data: r.items,
|
|
1070
1093
|
totalRecords: r.totalItems,
|
|
@@ -1222,7 +1245,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1222
1245
|
createClicked: "createClicked"
|
|
1223
1246
|
},
|
|
1224
1247
|
ngImport: i0,
|
|
1225
|
-
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. Deselect-all is the\n datatable's own header checkbox, which it shows only while a row is selected; the chip does\n not repeat it. Single selection has no checkbox column, so no header checkbox either, and a\n row click only ever selects: there the chip's ⊗ is the deselect. The bar keeps its height\n while empty, so ticking the first row does not push the table down 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 (selectionMode() === 'single') {\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 }\n </bs-badge>\n }\n </div>\n }\n\n <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n (filterChange)=\"onFilterChange($event)\"\n (rowClick)=\"onRowClick($event)\"\n (rowDblClick)=\"onRowDblClick($event)\"\n [(settings)]=\"settings\">\n @for (col of visibleColumns(); 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\">\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 && visibleColumns().length) {\n <div *bsDatatableColumn=\"'__sparkRowActions'; sortable: false; filterable: false\">\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 visibleColumns(); 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 <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\n (click)=\"chooseRowAction(action, row)\">\n {{ action.definition.displayName | 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",
|
|
1248
|
+
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 (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\">\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=\"'__sparkRowActions'; sortable: false; filterable: false\">\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 <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\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",
|
|
1226
1249
|
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"],
|
|
1227
1250
|
dependencies: [
|
|
1228
1251
|
{
|
|
@@ -1288,13 +1311,13 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1288
1311
|
"selectable",
|
|
1289
1312
|
"selection",
|
|
1290
1313
|
"rowKey",
|
|
1314
|
+
"rowLabel",
|
|
1291
1315
|
"resizableColumns",
|
|
1292
1316
|
"pagination",
|
|
1293
1317
|
"virtualScroll",
|
|
1294
1318
|
"itemSize",
|
|
1295
1319
|
"virtualBuffer",
|
|
1296
1320
|
"isResponsive",
|
|
1297
|
-
"compareWith",
|
|
1298
1321
|
"tree",
|
|
1299
1322
|
"idKey",
|
|
1300
1323
|
"childCountKey",
|
|
@@ -1513,7 +1536,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1513
1536
|
SparkIconComponent
|
|
1514
1537
|
],
|
|
1515
1538
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1516
|
-
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. Deselect-all is the\n datatable's own header checkbox, which it shows only while a row is selected; the chip does\n not repeat it. Single selection has no checkbox column, so no header checkbox either, and a\n row click only ever selects: there the chip's ⊗ is the deselect. The bar keeps its height\n while empty, so ticking the first row does not push the table down 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 (selectionMode() === 'single') {\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 }\n </bs-badge>\n }\n </div>\n }\n\n <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [distincts]=\"distinctsFn\"\n (filterChange)=\"onFilterChange($event)\"\n (rowClick)=\"onRowClick($event)\"\n (rowDblClick)=\"onRowDblClick($event)\"\n [(settings)]=\"settings\">\n @for (col of visibleColumns(); 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\">\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 && visibleColumns().length) {\n <div *bsDatatableColumn=\"'__sparkRowActions'; sortable: false; filterable: false\">\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 visibleColumns(); 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 <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\n (click)=\"chooseRowAction(action, row)\">\n {{ action.definition.displayName | 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",
|
|
1539
|
+
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 (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\">\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=\"'__sparkRowActions'; sortable: false; filterable: false\">\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 <li bsDropdownItem class=\"spark-row-action\" [attr.data-action]=\"action.name\"\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",
|
|
1517
1540
|
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"]
|
|
1518
1541
|
}]
|
|
1519
1542
|
}],
|
|
@@ -1965,7 +1988,7 @@ var SparkQueryCardComponent = class SparkQueryCardComponent {
|
|
|
1965
1988
|
actionsTpl = computed(() => slotFor(this.actionSlots(), this.query())?.templateRef ?? this.actionsTemplate(), ...ngDevMode ? [{ debugName: "actionsTpl" }] : /* istanbul ignore next */ []);
|
|
1966
1989
|
caption = computed(() => {
|
|
1967
1990
|
const q = this.query();
|
|
1968
|
-
return (q?.
|
|
1991
|
+
return (q?.label ? this.lang.resolve(q.label) : "") || q?.name || "";
|
|
1969
1992
|
}, ...ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []);
|
|
1970
1993
|
customActions = computed(() => this.grid()?.visibleCustomActions() ?? [], ...ngDevMode ? [{ debugName: "customActions" }] : /* istanbul ignore next */ []);
|
|
1971
1994
|
toolbarActions = computed(() => this.grid()?.toolbarActions() ?? [], ...ngDevMode ? [{ debugName: "toolbarActions" }] : /* istanbul ignore next */ []);
|
|
@@ -2123,7 +2146,7 @@ var SparkQueryCardComponent = class SparkQueryCardComponent {
|
|
|
2123
2146
|
isSignal: true
|
|
2124
2147
|
}],
|
|
2125
2148
|
ngImport: i0,
|
|
2126
|
-
template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly — which\n is what the old `showCard=\"false\"` meant, expressed as a component choice rather than a flag.\n-->\n<bs-card class=\"d-block my-3\">\n <bs-card-header>\n <div class=\"d-flex align-items-center gap-2\">\n @if (iconTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: query() }\"></ng-container>\n }\n\n <!--\n Caption first, actions on the trailing edge (#460 M15, as query-grid-card-PRD.md always said,\n and as Vidyano's sub-query tabs do). `me-auto` on the caption pushes the actions right; a card\n without actions looks exactly as a plain caption did.\n -->\n <span class=\"me-auto spark-query-card-caption\">\n @if (captionTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: caption(), query: query() }\"></ng-container>\n } @else {\n {{ caption() }}\n }\n </span>\n\n @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), actions: toolbarActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (toolbarActions().length) {\n <!-- The default: New, Delete and the custom actions the server offers this caller, with the\n overflow in the priority nav under the translated \"More\" (common.more). The nav renders only when there are any.\n\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n <bs-priority-nav class=\"spark-query-card-actions\" [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of toolbarActions(); track action.name) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"actionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isEnabled(action)\" (click)=\"run(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.
|
|
2149
|
+
template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly — which\n is what the old `showCard=\"false\"` meant, expressed as a component choice rather than a flag.\n-->\n<bs-card class=\"d-block my-3\">\n <bs-card-header>\n <div class=\"d-flex align-items-center gap-2\">\n @if (iconTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: query() }\"></ng-container>\n }\n\n <!--\n Caption first, actions on the trailing edge (#460 M15, as query-grid-card-PRD.md always said,\n and as Vidyano's sub-query tabs do). `me-auto` on the caption pushes the actions right; a card\n without actions looks exactly as a plain caption did.\n -->\n <span class=\"me-auto spark-query-card-caption\">\n @if (captionTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: caption(), query: query() }\"></ng-container>\n } @else {\n {{ caption() }}\n }\n </span>\n\n @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), actions: toolbarActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (toolbarActions().length) {\n <!-- The default: New, Delete and the custom actions the server offers this caller, with the\n overflow in the priority nav under the translated \"More\" (common.more). The nav renders only when there are any.\n\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n <bs-priority-nav class=\"spark-query-card-actions\" [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of toolbarActions(); track action.name) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"actionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isEnabled(action)\" (click)=\"run(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.label | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n }\n\n <!-- The query search, on the trailing edge next to the actions, as Vidyano's sub-query tabs\n have it (#460 M15, D17 addendum). The same box as the query-list page; it feeds the\n grid's `search`, which the server applies together with the parent. Absent for bound\n `data`, which the grid never refetches. -->\n @if (showSearch()) {\n <spark-search-box class=\"spark-query-card-search\" size=\"sm\" [(term)]=\"searchTerm\" />\n }\n </div>\n </bs-card-header>\n\n <div class=\"p-3\">\n <spark-query-grid\n [queryId]=\"queryId()\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [reloadToken]=\"reloadToken()\"\n [data]=\"data()\"\n [search]=\"searchTerm()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n [deleted]=\"deleted()\"\n [parentDeleted]=\"parentDeleted()\"\n (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n",
|
|
2127
2150
|
styles: [".spark-query-card-search{flex:0 1 14rem;min-width:8rem}\n"],
|
|
2128
2151
|
dependencies: [
|
|
2129
2152
|
{
|
|
@@ -2248,7 +2271,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
2248
2271
|
SparkIconComponent
|
|
2249
2272
|
],
|
|
2250
2273
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2251
|
-
template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly — which\n is what the old `showCard=\"false\"` meant, expressed as a component choice rather than a flag.\n-->\n<bs-card class=\"d-block my-3\">\n <bs-card-header>\n <div class=\"d-flex align-items-center gap-2\">\n @if (iconTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: query() }\"></ng-container>\n }\n\n <!--\n Caption first, actions on the trailing edge (#460 M15, as query-grid-card-PRD.md always said,\n and as Vidyano's sub-query tabs do). `me-auto` on the caption pushes the actions right; a card\n without actions looks exactly as a plain caption did.\n -->\n <span class=\"me-auto spark-query-card-caption\">\n @if (captionTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: caption(), query: query() }\"></ng-container>\n } @else {\n {{ caption() }}\n }\n </span>\n\n @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), actions: toolbarActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (toolbarActions().length) {\n <!-- The default: New, Delete and the custom actions the server offers this caller, with the\n overflow in the priority nav under the translated \"More\" (common.more). The nav renders only when there are any.\n\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n <bs-priority-nav class=\"spark-query-card-actions\" [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of toolbarActions(); track action.name) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"actionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isEnabled(action)\" (click)=\"run(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.
|
|
2274
|
+
template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly — which\n is what the old `showCard=\"false\"` meant, expressed as a component choice rather than a flag.\n-->\n<bs-card class=\"d-block my-3\">\n <bs-card-header>\n <div class=\"d-flex align-items-center gap-2\">\n @if (iconTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: query() }\"></ng-container>\n }\n\n <!--\n Caption first, actions on the trailing edge (#460 M15, as query-grid-card-PRD.md always said,\n and as Vidyano's sub-query tabs do). `me-auto` on the caption pushes the actions right; a card\n without actions looks exactly as a plain caption did.\n -->\n <span class=\"me-auto spark-query-card-caption\">\n @if (captionTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: caption(), query: query() }\"></ng-container>\n } @else {\n {{ caption() }}\n }\n </span>\n\n @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), actions: toolbarActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (toolbarActions().length) {\n <!-- The default: New, Delete and the custom actions the server offers this caller, with the\n overflow in the priority nav under the translated \"More\" (common.more). The nav renders only when there are any.\n\n Disabled, not hidden: hiding makes the affordance undiscoverable and reflows the\n priority nav on every selection change. The server enforces the same rules. -->\n <bs-priority-nav class=\"spark-query-card-actions\" [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of toolbarActions(); track action.name) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"actionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isEnabled(action)\" (click)=\"run(action)\">\n @if (action.definition.icon && action.kind !== 'custom') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.label | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n }\n\n <!-- The query search, on the trailing edge next to the actions, as Vidyano's sub-query tabs\n have it (#460 M15, D17 addendum). The same box as the query-list page; it feeds the\n grid's `search`, which the server applies together with the parent. Absent for bound\n `data`, which the grid never refetches. -->\n @if (showSearch()) {\n <spark-search-box class=\"spark-query-card-search\" size=\"sm\" [(term)]=\"searchTerm\" />\n }\n </div>\n </bs-card-header>\n\n <div class=\"p-3\">\n <spark-query-grid\n [queryId]=\"queryId()\"\n [parentId]=\"parentId()\"\n [parentType]=\"parentType()\"\n [reloadToken]=\"reloadToken()\"\n [data]=\"data()\"\n [search]=\"searchTerm()\"\n [rowRoute]=\"rowRoute()\"\n [selectionModeSetting]=\"selectionMode()\"\n [deleted]=\"deleted()\"\n [parentDeleted]=\"parentDeleted()\"\n (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n",
|
|
2252
2275
|
styles: [".spark-query-card-search{flex:0 1 14rem;min-width:8rem}\n"]
|
|
2253
2276
|
}]
|
|
2254
2277
|
}],
|
|
@@ -2395,6 +2418,6 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
2395
2418
|
}]
|
|
2396
2419
|
}
|
|
2397
2420
|
});
|
|
2398
|
-
export { SPARK_GRID_PAGE_SIZES, SPARK_SEARCH_DEBOUNCE_MS, SparkGridCellComponent, SparkGridRenderers, SparkQueryActionsContext, SparkQueryActionsDirective, SparkQueryCaptionContext, SparkQueryCaptionDirective, SparkQueryCardComponent, SparkQueryGridComponent, SparkQueryIconDirective, SparkQuerySlotContext, SparkSearchBoxComponent, applicablePresetFilters, initialGridSettings, isVirtualScrollingQuery, slotFor, sparkActionClass
|
|
2421
|
+
export { SPARK_GRID_PAGE_SIZES, SPARK_SEARCH_DEBOUNCE_MS, SparkGridCellComponent, SparkGridRenderers, SparkQueryActionsContext, SparkQueryActionsDirective, SparkQueryCaptionContext, SparkQueryCaptionDirective, SparkQueryCardComponent, SparkQueryGridComponent, SparkQueryIconDirective, SparkQuerySlotContext, SparkSearchBoxComponent, applicablePresetFilters, initialGridSettings, isVirtualScrollingQuery, slotFor, sparkActionClass };
|
|
2399
2422
|
|
|
2400
2423
|
//# sourceMappingURL=mintplayer-ng-spark-grid.mjs.map
|