@mintplayer/ng-spark 22.28.1 → 22.30.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 +1 -1
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs +82 -4
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-grid.mjs +59 -18
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +2 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +14 -4
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +6 -3
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +5 -2
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +109 -4
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs +36 -6
- package/fesm2022/mintplayer-ng-spark-retry-action-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +37 -5
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/package.json +3 -3
- package/types/mintplayer-ng-spark-client-operations.d.ts +81 -4
- package/types/mintplayer-ng-spark-grid.d.ts +36 -4
- package/types/mintplayer-ng-spark-models.d.ts +28 -0
- package/types/mintplayer-ng-spark-po-detail.d.ts +8 -0
- package/types/mintplayer-ng-spark-po-form.d.ts +2 -0
- package/types/mintplayer-ng-spark-query-list.d.ts +2 -0
- package/types/mintplayer-ng-spark-renderers.d.ts +47 -0
- package/types/mintplayer-ng-spark-retry-action-modal.d.ts +1 -0
- package/types/mintplayer-ng-spark-services.d.ts +27 -1
|
@@ -3,7 +3,7 @@ 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 } from "@mintplayer/ng-spark/services";
|
|
6
|
+
import { SPARK_RETURN_URL_STATE_KEY, SparkLanguageService, SparkService, sparkDatatableLabels } from "@mintplayer/ng-spark/services";
|
|
7
7
|
import * as i1$1 from "@angular/common";
|
|
8
8
|
import { CommonModule, NgComponentOutlet } from "@angular/common";
|
|
9
9
|
import { toSignal } from "@angular/core/rxjs-interop";
|
|
@@ -17,7 +17,7 @@ import { BsBadgeComponent } from "@mintplayer/ng-bootstrap/badge";
|
|
|
17
17
|
import { CdkConnectedOverlay, CdkOverlayOrigin } from "@angular/cdk/overlay";
|
|
18
18
|
import { BsDropdownItemDirective, BsDropdownMenuComponent } from "@mintplayer/ng-bootstrap/dropdown-menu";
|
|
19
19
|
import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
|
|
20
|
-
import { SparkQueryRefreshService } from "@mintplayer/ng-spark/client-operations";
|
|
20
|
+
import { SparkClientMethodRegistry, SparkQueryRefreshService } from "@mintplayer/ng-spark/client-operations";
|
|
21
21
|
import { QueryCellValuePipe, QueryReferenceChipsPipe, ResolveTranslationPipe, TranslateKeyPipe } from "@mintplayer/ng-spark/pipes";
|
|
22
22
|
import { SparkAttributeDescriptionComponent } from "@mintplayer/ng-spark/attribute-description";
|
|
23
23
|
import { SparkColumnFilterPanelComponent } from "@mintplayer/ng-spark/column-filter";
|
|
@@ -522,6 +522,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
522
522
|
injector = inject(Injector);
|
|
523
523
|
addonRowActions = orderSparkExtensions(inject(SPARK_QUERY_ROW_ACTIONS, { optional: true }), (a) => a.priority ?? 50);
|
|
524
524
|
lang = inject(SparkLanguageService);
|
|
525
|
+
clientMethods = inject(SparkClientMethodRegistry);
|
|
525
526
|
queryId = input.required(...ngDevMode ? [{ debugName: "queryId" }] : /* istanbul ignore next */ []);
|
|
526
527
|
parentId = input("", ...ngDevMode ? [{ debugName: "parentId" }] : /* istanbul ignore next */ []);
|
|
527
528
|
parentType = input("", ...ngDevMode ? [{ debugName: "parentType" }] : /* istanbul ignore next */ []);
|
|
@@ -614,6 +615,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
614
615
|
return this.customActions().filter((action) => !lowered.has(action.name.toLowerCase()));
|
|
615
616
|
}, ...ngDevMode ? [{ debugName: "visibleCustomActions" }] : /* istanbul ignore next */ []);
|
|
616
617
|
fetchFn = signal(null, ...ngDevMode ? [{ debugName: "fetchFn" }] : /* istanbul ignore next */ []);
|
|
618
|
+
datatable = viewChild(BsDatatableComponent, ...ngDevMode ? [{ debugName: "datatable" }] : /* istanbul ignore next */ []);
|
|
617
619
|
errorMessage = signal(null, ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
|
|
618
620
|
offeredDefaultActions = computed(() => {
|
|
619
621
|
if (this.recycleBin()) return [];
|
|
@@ -753,6 +755,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
753
755
|
this.closeRowMenu(rowId);
|
|
754
756
|
}
|
|
755
757
|
async chooseRowAction(action, row) {
|
|
758
|
+
if (this.actionUnavailableReason(action.definition) !== null) return;
|
|
756
759
|
this.closeRowMenu();
|
|
757
760
|
await this.runRowAction(action, row);
|
|
758
761
|
}
|
|
@@ -841,9 +844,15 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
841
844
|
isVirtualScrolling = computed(() => isVirtualScrollingQuery(this.query()), ...ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []);
|
|
842
845
|
fetchedColumns = signal([], ...ngDevMode ? [{ debugName: "fetchedColumns" }] : /* istanbul ignore next */ []);
|
|
843
846
|
allColumns = computed(() => this.columns() ?? this.fetchedColumns(), ...ngDevMode ? [{ debugName: "allColumns" }] : /* istanbul ignore next */ []);
|
|
847
|
+
rowActionsColumn = "__sparkRowActions";
|
|
848
|
+
datatableLabels = computed(() => sparkDatatableLabels(this.lang), ...ngDevMode ? [{ debugName: "datatableLabels" }] : /* istanbul ignore next */ []);
|
|
844
849
|
hasExternalData = computed(() => this.data() !== null || this.query()?.isStreamingQuery === true, ...ngDevMode ? [{ debugName: "hasExternalData" }] : /* istanbul ignore next */ []);
|
|
845
850
|
isActionEnabled(action) {
|
|
846
|
-
return parseSelectionRule(action.selectionRule)(this.selection().map((r) => r.id).length);
|
|
851
|
+
return this.clientMethods.unavailableReason(action.requiresClient) === null && parseSelectionRule(action.selectionRule)(this.selection().map((r) => r.id).length);
|
|
852
|
+
}
|
|
853
|
+
actionUnavailableReason(action) {
|
|
854
|
+
const key = this.clientMethods.unavailableReason(action.requiresClient);
|
|
855
|
+
return key === null ? null : this.lang.t(key);
|
|
847
856
|
}
|
|
848
857
|
constructor() {
|
|
849
858
|
effect(() => {
|
|
@@ -854,14 +863,27 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
854
863
|
else this.loading.set(false);
|
|
855
864
|
});
|
|
856
865
|
let first = true;
|
|
866
|
+
let lastReloadToken;
|
|
867
|
+
const seen = /* @__PURE__ */ new Map();
|
|
857
868
|
effect(() => {
|
|
858
|
-
this.reloadToken();
|
|
859
|
-
this.
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
869
|
+
const reloadToken = this.reloadToken();
|
|
870
|
+
const query = this.query();
|
|
871
|
+
let refreshed = false;
|
|
872
|
+
for (const key of [
|
|
873
|
+
this.queryId(),
|
|
874
|
+
query?.id,
|
|
875
|
+
query?.alias
|
|
876
|
+
]) {
|
|
877
|
+
if (!key) continue;
|
|
878
|
+
const token = this.queryRefresh.tokenFor(key);
|
|
879
|
+
const last = seen.get(key.toLowerCase());
|
|
880
|
+
if (last !== void 0 && token > last) refreshed = true;
|
|
881
|
+
seen.set(key.toLowerCase(), token);
|
|
863
882
|
}
|
|
864
|
-
|
|
883
|
+
const hostRequested = !first && !Object.is(reloadToken, lastReloadToken);
|
|
884
|
+
lastReloadToken = reloadToken;
|
|
885
|
+
first = false;
|
|
886
|
+
if (refreshed || hostRequested) untracked(() => this.reload());
|
|
865
887
|
});
|
|
866
888
|
let firstSearch = true;
|
|
867
889
|
effect(() => {
|
|
@@ -908,10 +930,13 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
908
930
|
});
|
|
909
931
|
});
|
|
910
932
|
}
|
|
911
|
-
reload() {
|
|
933
|
+
reload(options) {
|
|
912
934
|
if (this.hasExternalData()) return;
|
|
913
935
|
const q = this.query();
|
|
914
|
-
if (q)
|
|
936
|
+
if (!q) return;
|
|
937
|
+
const datatable = this.datatable();
|
|
938
|
+
if (datatable && this.fetchFn()) datatable.reload(options);
|
|
939
|
+
else this.fetchFn.set(this.makeFetch(q, this.parentId(), this.parentType()));
|
|
915
940
|
}
|
|
916
941
|
onSearchChanged() {
|
|
917
942
|
if (this.hasExternalData()) return;
|
|
@@ -994,7 +1019,7 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
994
1019
|
},
|
|
995
1020
|
sortColumns: s.sortColumns
|
|
996
1021
|
}));
|
|
997
|
-
this.reload();
|
|
1022
|
+
this.reload({ resetPage: true });
|
|
998
1023
|
}
|
|
999
1024
|
async onCustomAction(action, rowIds) {
|
|
1000
1025
|
const message = confirmationText(action, rowIds?.length ?? this.selection().length);
|
|
@@ -1244,8 +1269,15 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1244
1269
|
customActionExecuted: "customActionExecuted",
|
|
1245
1270
|
createClicked: "createClicked"
|
|
1246
1271
|
},
|
|
1272
|
+
viewQueries: [{
|
|
1273
|
+
propertyName: "datatable",
|
|
1274
|
+
first: true,
|
|
1275
|
+
predicate: BsDatatableComponent,
|
|
1276
|
+
descendants: true,
|
|
1277
|
+
isSignal: true
|
|
1278
|
+
}],
|
|
1247
1279
|
ngImport: i0,
|
|
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",
|
|
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",
|
|
1249
1281
|
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"],
|
|
1250
1282
|
dependencies: [
|
|
1251
1283
|
{
|
|
@@ -1344,6 +1376,8 @@ var SparkQueryGridComponent = class SparkQueryGridComponent {
|
|
|
1344
1376
|
inputs: [
|
|
1345
1377
|
"bsDatatableColumn",
|
|
1346
1378
|
"bsDatatableColumnSortable",
|
|
1379
|
+
"bsDatatableColumnLabel",
|
|
1380
|
+
"bsDatatableColumnResizable",
|
|
1347
1381
|
"bsDatatableColumnFilterable",
|
|
1348
1382
|
"bsDatatableColumnFilterActive",
|
|
1349
1383
|
"bsDatatableColumnFilterSummary",
|
|
@@ -1536,7 +1570,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1536
1570
|
SparkIconComponent
|
|
1537
1571
|
],
|
|
1538
1572
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
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",
|
|
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",
|
|
1540
1574
|
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"]
|
|
1541
1575
|
}]
|
|
1542
1576
|
}],
|
|
@@ -1675,6 +1709,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1675
1709
|
createClicked: [{
|
|
1676
1710
|
type: i0.Output,
|
|
1677
1711
|
args: ["createClicked"]
|
|
1712
|
+
}],
|
|
1713
|
+
datatable: [{
|
|
1714
|
+
type: i0.ViewChild,
|
|
1715
|
+
args: [i0.forwardRef(() => BsDatatableComponent), { isSignal: true }]
|
|
1678
1716
|
}]
|
|
1679
1717
|
}
|
|
1680
1718
|
});
|
|
@@ -1998,6 +2036,9 @@ var SparkQueryCardComponent = class SparkQueryCardComponent {
|
|
|
1998
2036
|
isEnabled(action) {
|
|
1999
2037
|
return this.grid()?.isToolbarActionEnabled(action) ?? false;
|
|
2000
2038
|
}
|
|
2039
|
+
unavailableReason(action) {
|
|
2040
|
+
return this.grid()?.actionUnavailableReason(action.definition) ?? null;
|
|
2041
|
+
}
|
|
2001
2042
|
run(action) {
|
|
2002
2043
|
this.grid()?.runToolbarAction(action);
|
|
2003
2044
|
}
|
|
@@ -2146,8 +2187,8 @@ var SparkQueryCardComponent = class SparkQueryCardComponent {
|
|
|
2146
2187
|
isSignal: true
|
|
2147
2188
|
}],
|
|
2148
2189
|
ngImport: i0,
|
|
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",
|
|
2150
|
-
styles: [".spark-query-card-search{flex:0 1 14rem;min-width:8rem}\n"],
|
|
2190
|
+
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=\"spark-query-card-header 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)\" [attr.title]=\"unavailableReason(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",
|
|
2191
|
+
styles: [".spark-query-card-search{flex:0 1 14rem;min-width:8rem}@media(max-width:575.98px){.spark-query-card-header{flex-wrap:wrap}.spark-query-card-search{flex:1 1 8rem}}\n"],
|
|
2151
2192
|
dependencies: [
|
|
2152
2193
|
{
|
|
2153
2194
|
kind: "ngmodule",
|
|
@@ -2271,8 +2312,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
2271
2312
|
SparkIconComponent
|
|
2272
2313
|
],
|
|
2273
2314
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
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",
|
|
2275
|
-
styles: [".spark-query-card-search{flex:0 1 14rem;min-width:8rem}\n"]
|
|
2315
|
+
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=\"spark-query-card-header 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)\" [attr.title]=\"unavailableReason(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",
|
|
2316
|
+
styles: [".spark-query-card-search{flex:0 1 14rem;min-width:8rem}@media(max-width:575.98px){.spark-query-card-header{flex-wrap:wrap}.spark-query-card-search{flex:1 1 8rem}}\n"]
|
|
2276
2317
|
}]
|
|
2277
2318
|
}],
|
|
2278
2319
|
propDecorators: {
|