@mintplayer/ng-spark 22.6.0 → 22.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/mintplayer-ng-spark-attribute-description.mjs +68 -0
- package/fesm2022/mintplayer-ng-spark-attribute-description.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-grid.mjs +134 -52
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +77 -7
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +87 -4
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +4 -3
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +15 -16
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +21 -7
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +16 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-services.mjs +92 -5
- package/fesm2022/mintplayer-ng-spark-services.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-shell.mjs +41 -4
- package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
- package/package.json +5 -1
- package/types/mintplayer-ng-spark-attribute-description.d.ts +30 -0
- package/types/mintplayer-ng-spark-grid.d.ts +75 -33
- package/types/mintplayer-ng-spark-models.d.ts +136 -7
- package/types/mintplayer-ng-spark-pipes.d.ts +33 -5
- package/types/mintplayer-ng-spark-po-detail.d.ts +2 -2
- package/types/mintplayer-ng-spark-po-form.d.ts +7 -7
- package/types/mintplayer-ng-spark-query-list.d.ts +11 -4
- package/types/mintplayer-ng-spark-renderers.d.ts +31 -11
- package/types/mintplayer-ng-spark-services.d.ts +71 -2
- package/types/mintplayer-ng-spark-shell.d.ts +32 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, computed, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { BsTooltipDirective } from '@mintplayer/ng-bootstrap/tooltip';
|
|
4
|
+
import { SparkIconComponent } from '@mintplayer/ng-spark/icon';
|
|
5
|
+
import { resolveTranslation, currentLanguage } from '@mintplayer/ng-spark/models';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The [i] beside an attribute label: a focusable button whose tooltip shows the attribute's
|
|
9
|
+
* `description` (#348). Renders nothing when the model declares no description, so every label
|
|
10
|
+
* site can include it unconditionally.
|
|
11
|
+
*
|
|
12
|
+
* Accessibility comes from `*bsTooltip`: it opens on hover AND focus, closes on Escape, and sets
|
|
13
|
+
* `aria-describedby` on the button while open. The button's own name is the description text, so
|
|
14
|
+
* a screen reader announces the help on focus without waiting for the tooltip.
|
|
15
|
+
*
|
|
16
|
+
* Clicks are stopped: the [i] lives inside sortable grid headers and `<label for>` elements, and
|
|
17
|
+
* must neither toggle the sort nor move focus into the input.
|
|
18
|
+
*/
|
|
19
|
+
class SparkAttributeDescriptionComponent {
|
|
20
|
+
/** The attribute's `description`; `undefined` (the common case) renders nothing. */
|
|
21
|
+
description = input(/* @ts-ignore */
|
|
22
|
+
...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
|
|
23
|
+
/** Where the tooltip opens relative to the [i]. */
|
|
24
|
+
position = input('top', /* @ts-ignore */
|
|
25
|
+
...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
26
|
+
/** Resolved for the current language; re-evaluates when the user switches language. */
|
|
27
|
+
text = computed(() => resolveTranslation(this.description(), currentLanguage()).trim(), /* @ts-ignore */
|
|
28
|
+
...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
29
|
+
onClick(event) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
event.stopPropagation();
|
|
32
|
+
}
|
|
33
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkAttributeDescriptionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
34
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkAttributeDescriptionComponent, isStandalone: true, selector: "spark-attribute-description", inputs: { description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
35
|
+
@if (text(); as text) {
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
class="btn btn-link p-0 ms-1 align-baseline spark-attribute-description-trigger"
|
|
39
|
+
[attr.aria-label]="text"
|
|
40
|
+
(click)="onClick($event)">
|
|
41
|
+
<spark-icon name="info-circle" />
|
|
42
|
+
<span *bsTooltip="position()" class="spark-attribute-description-text">{{ text }}</span>
|
|
43
|
+
</button>
|
|
44
|
+
}
|
|
45
|
+
`, isInline: true, styles: [":host{display:inline-block;line-height:1}.spark-attribute-description-trigger{color:inherit;opacity:.6;font-size:.875em;vertical-align:baseline}.spark-attribute-description-trigger:hover,.spark-attribute-description-trigger:focus-visible{opacity:1}.spark-attribute-description-text{display:inline-block;white-space:pre-line;text-align:start;max-width:20rem}\n"], dependencies: [{ kind: "directive", type: BsTooltipDirective, selector: "*[bsTooltip]", inputs: ["bsTooltip"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
46
|
+
}
|
|
47
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkAttributeDescriptionComponent, decorators: [{
|
|
48
|
+
type: Component,
|
|
49
|
+
args: [{ selector: 'spark-attribute-description', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [BsTooltipDirective, SparkIconComponent], template: `
|
|
50
|
+
@if (text(); as text) {
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
class="btn btn-link p-0 ms-1 align-baseline spark-attribute-description-trigger"
|
|
54
|
+
[attr.aria-label]="text"
|
|
55
|
+
(click)="onClick($event)">
|
|
56
|
+
<spark-icon name="info-circle" />
|
|
57
|
+
<span *bsTooltip="position()" class="spark-attribute-description-text">{{ text }}</span>
|
|
58
|
+
</button>
|
|
59
|
+
}
|
|
60
|
+
`, styles: [":host{display:inline-block;line-height:1}.spark-attribute-description-trigger{color:inherit;opacity:.6;font-size:.875em;vertical-align:baseline}.spark-attribute-description-trigger:hover,.spark-attribute-description-trigger:focus-visible{opacity:1}.spark-attribute-description-text{display:inline-block;white-space:pre-line;text-align:start;max-width:20rem}\n"] }]
|
|
61
|
+
}], propDecorators: { description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }] } });
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generated bundle index. Do not edit.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
export { SparkAttributeDescriptionComponent };
|
|
68
|
+
//# sourceMappingURL=mintplayer-ng-spark-attribute-description.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-attribute-description.mjs","sources":["../../attribute-description/src/spark-attribute-description.component.ts","../../attribute-description/mintplayer-ng-spark-attribute-description.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { BsTooltipDirective } from '@mintplayer/ng-bootstrap/tooltip';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { TranslatedString, currentLanguage, resolveTranslation } from '@mintplayer/ng-spark/models';\n\nexport type SparkAttributeDescriptionPosition = 'top' | 'bottom' | 'start' | 'end';\n\n/**\n * The [i] beside an attribute label: a focusable button whose tooltip shows the attribute's\n * `description` (#348). Renders nothing when the model declares no description, so every label\n * site can include it unconditionally.\n *\n * Accessibility comes from `*bsTooltip`: it opens on hover AND focus, closes on Escape, and sets\n * `aria-describedby` on the button while open. The button's own name is the description text, so\n * a screen reader announces the help on focus without waiting for the tooltip.\n *\n * Clicks are stopped: the [i] lives inside sortable grid headers and `<label for>` elements, and\n * must neither toggle the sort nor move focus into the input.\n */\n@Component({\n selector: 'spark-attribute-description',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [BsTooltipDirective, SparkIconComponent],\n template: `\n @if (text(); as text) {\n <button\n type=\"button\"\n class=\"btn btn-link p-0 ms-1 align-baseline spark-attribute-description-trigger\"\n [attr.aria-label]=\"text\"\n (click)=\"onClick($event)\">\n <spark-icon name=\"info-circle\" />\n <span *bsTooltip=\"position()\" class=\"spark-attribute-description-text\">{{ text }}</span>\n </button>\n }\n `,\n styles: [`\n :host {\n display: inline-block;\n line-height: 1;\n }\n .spark-attribute-description-trigger {\n color: inherit;\n opacity: 0.6;\n font-size: 0.875em;\n vertical-align: baseline;\n }\n .spark-attribute-description-trigger:hover,\n .spark-attribute-description-trigger:focus-visible {\n opacity: 1;\n }\n .spark-attribute-description-text {\n display: inline-block;\n white-space: pre-line;\n text-align: start;\n max-width: 20rem;\n }\n `],\n})\nexport class SparkAttributeDescriptionComponent {\n /** The attribute's `description`; `undefined` (the common case) renders nothing. */\n description = input<TranslatedString | undefined>();\n\n /** Where the tooltip opens relative to the [i]. */\n position = input<SparkAttributeDescriptionPosition>('top');\n\n /** Resolved for the current language; re-evaluates when the user switches language. */\n text = computed(() => resolveTranslation(this.description(), currentLanguage()).trim());\n\n onClick(event: MouseEvent): void {\n event.preventDefault();\n event.stopPropagation();\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;AAOA;;;;;;;;;;;AAWG;MAyCU,kCAAkC,CAAA;;AAE7C,IAAA,WAAW,GAAG,KAAK;+FAAgC;;IAGnD,QAAQ,GAAG,KAAK,CAAoC,KAAK;iFAAC;;AAG1D,IAAA,IAAI,GAAG,QAAQ,CAAC,MAAM,kBAAkB,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC,IAAI,EAAE;6EAAC;AAEvF,IAAA,OAAO,CAAC,KAAiB,EAAA;QACvB,KAAK,CAAC,cAAc,EAAE;QACtB,KAAK,CAAC,eAAe,EAAE;IACzB;uGAbW,kCAAkC,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAlC,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,kCAAkC,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,6BAAA,EAAA,MAAA,EAAA,EAAA,WAAA,EAAA,EAAA,iBAAA,EAAA,aAAA,EAAA,UAAA,EAAA,aAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,QAAA,EAAA,EAAA,iBAAA,EAAA,UAAA,EAAA,UAAA,EAAA,UAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EAnCnC;;;;;;;;;;;GAWT,EAAA,QAAA,EAAA,IAAA,EAAA,MAAA,EAAA,CAAA,yWAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAZS,kBAAkB,gFAAE,kBAAkB,EAAA,QAAA,EAAA,YAAA,EAAA,MAAA,EAAA,CAAA,MAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FAoCrC,kCAAkC,EAAA,UAAA,EAAA,CAAA;kBAxC9C,SAAS;AACE,YAAA,IAAA,EAAA,CAAA,EAAA,QAAA,EAAA,6BAA6B,EAAA,UAAA,EAC3B,IAAI,EAAA,eAAA,EACC,uBAAuB,CAAC,MAAM,EAAA,OAAA,EACtC,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,EAAA,QAAA,EACvC;;;;;;;;;;;AAWT,EAAA,CAAA,EAAA,MAAA,EAAA,CAAA,yWAAA,CAAA,EAAA;;;ACnCH;;AAEG;;;;"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DatatableSettings, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective } from '@mintplayer/ng-bootstrap/datatable';
|
|
2
|
-
import { hasShowedOnFlag, ShowedOn, selectionModeFor, parseSelectionRule, filterQueryActions } from '@mintplayer/ng-spark/models';
|
|
2
|
+
import { hasShowedOnFlag, ShowedOn, valueFor, selectionModeFor, parseSelectionRule, filterQueryActions } from '@mintplayer/ng-spark/models';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
4
|
import { inject, Injectable, TemplateRef, input, Directive, computed, ChangeDetectionStrategy, Component, model, output, signal, effect, untracked, contentChildren, viewChild } from '@angular/core';
|
|
5
|
-
import { SPARK_ATTRIBUTE_RENDERERS,
|
|
5
|
+
import { SPARK_ATTRIBUTE_RENDERERS, cellValue, withDeclaredInputs } from '@mintplayer/ng-spark/renderers';
|
|
6
6
|
import { SparkService, SparkLanguageService } from '@mintplayer/ng-spark/services';
|
|
7
7
|
import * as i1 from '@angular/common';
|
|
8
8
|
import { CommonModule, NgComponentOutlet } from '@angular/common';
|
|
@@ -12,7 +12,8 @@ import { Color } from '@mintplayer/ng-bootstrap';
|
|
|
12
12
|
import { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';
|
|
13
13
|
import { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';
|
|
14
14
|
import { SparkQueryRefreshService } from '@mintplayer/ng-spark/client-operations';
|
|
15
|
-
import { ResolveTranslationPipe,
|
|
15
|
+
import { ResolveTranslationPipe, QueryCellValuePipe, QueryReferenceChipsPipe, TranslateKeyPipe } from '@mintplayer/ng-spark/pipes';
|
|
16
|
+
import { SparkAttributeDescriptionComponent } from '@mintplayer/ng-spark/attribute-description';
|
|
16
17
|
import { BsCardComponent, BsCardHeaderComponent } from '@mintplayer/ng-bootstrap/card';
|
|
17
18
|
import { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';
|
|
18
19
|
|
|
@@ -66,55 +67,50 @@ function isVirtualScrollingQuery(query) {
|
|
|
66
67
|
* There is now one grid, {@link SparkQueryGridComponent}, so the drift cannot recur. This stays
|
|
67
68
|
* separate because it is stateless service-shaped logic rather than view state, and because it is
|
|
68
69
|
* what a custom grid would need in order to render Spark cells at all.
|
|
69
|
-
*
|
|
70
|
-
* The constraint that shaped the split still holds and is why the grid takes rows as an input
|
|
71
|
-
* rather than owning a socket: streaming's dependency graph must not reach the bundle of every
|
|
72
|
-
* detail page.
|
|
73
70
|
*/
|
|
74
71
|
class SparkGridRenderers {
|
|
75
72
|
registry = inject(SPARK_ATTRIBUTE_RENDERERS);
|
|
76
73
|
sparkService = inject(SparkService);
|
|
77
|
-
/** The registered column component for
|
|
78
|
-
columnComponentFor(
|
|
79
|
-
if (!
|
|
74
|
+
/** The registered column component for a column, or null to fall back to the default cell. */
|
|
75
|
+
columnComponentFor(column) {
|
|
76
|
+
if (!column.renderer)
|
|
80
77
|
return null;
|
|
81
|
-
return this.registry.find(r => r.name ===
|
|
78
|
+
return this.registry.find(r => r.name === column.renderer)?.columnComponent ?? null;
|
|
82
79
|
}
|
|
83
80
|
/**
|
|
84
|
-
* Inputs for a
|
|
81
|
+
* Inputs for a query-grid cell renderer, filtered to what the component actually declares —
|
|
85
82
|
* `NgComponentOutlet` throws on an input the target does not have, which is what lets every
|
|
86
83
|
* member of the renderer contract be optional.
|
|
87
84
|
*/
|
|
88
|
-
columnInputsFor(component, item,
|
|
89
|
-
|
|
90
|
-
return this.cellInputsFor(component, rendererValue(itemAttr), attr, item);
|
|
85
|
+
columnInputsFor(component, item, column) {
|
|
86
|
+
return this.cellInputsFor(component, cellValue(valueFor(item, column.name)), column, item);
|
|
91
87
|
}
|
|
92
88
|
/**
|
|
93
|
-
* The same contract for a row that is not a
|
|
89
|
+
* The same contract for a row that is not a query result — an AsDetail sub-table row, which is
|
|
90
|
+
* a plain dictionary of embedded values with no id and no column metadata of its own, so its
|
|
91
|
+
* value cannot be read the way {@link columnInputsFor} reads one.
|
|
94
92
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* differs; the renderer contract is identical, and stating it once is what stops the two from
|
|
98
|
-
* drifting the way the cell markup already had.
|
|
93
|
+
* Only the extraction differs; the renderer contract is identical, and stating it once is what
|
|
94
|
+
* stops the two from drifting the way the cell markup already had.
|
|
99
95
|
*/
|
|
100
96
|
cellInputsFor(component, value, column, item) {
|
|
101
97
|
return withDeclaredInputs(component, {
|
|
102
98
|
value,
|
|
103
|
-
|
|
99
|
+
column,
|
|
104
100
|
options: column.rendererOptions,
|
|
105
101
|
item,
|
|
106
102
|
});
|
|
107
103
|
}
|
|
108
104
|
/**
|
|
109
|
-
* Loads every lookup reference the visible
|
|
105
|
+
* Loads every lookup reference the visible columns need, in one pass.
|
|
110
106
|
*
|
|
111
107
|
* Returns an empty map rather than throwing when there are none, so callers never branch on it.
|
|
112
108
|
*/
|
|
113
|
-
async loadLookupOptions(
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
109
|
+
async loadLookupOptions(columns) {
|
|
110
|
+
const lookupColumns = columns.filter(c => c.lookupReferenceType);
|
|
111
|
+
if (lookupColumns.length === 0)
|
|
116
112
|
return {};
|
|
117
|
-
const names = [...new Set(
|
|
113
|
+
const names = [...new Set(lookupColumns.map(c => c.lookupReferenceType))];
|
|
118
114
|
const entries = await Promise.all(names.map(async (name) => [name, await this.sparkService.getLookupReference(name)]));
|
|
119
115
|
return entries.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {});
|
|
120
116
|
}
|
|
@@ -338,11 +334,11 @@ class SparkGridCellComponent {
|
|
|
338
334
|
isUnsetBoolean = computed(() => this.display() == null, /* @ts-ignore */
|
|
339
335
|
...(ngDevMode ? [{ debugName: "isUnsetBoolean" }] : /* istanbul ignore next */ []));
|
|
340
336
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkGridCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
341
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkGridCellComponent, isStandalone: true, selector: "spark-grid-cell", inputs: { column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, rendererValue: { classPropertyName: "rendererValue", publicName: "rendererValue", isSignal: true, isRequired: false, transformFunction: null }, item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: false, transformFunction: null }, chips: { classPropertyName: "chips", publicName: "chips", isSignal: true, isRequired: false, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (rendererComponent(); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: rendererInputs()\"></ng-container>\n} @else if (column().dataType === 'boolean') {\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked box, which reads\n as an explicit `false`. One of the three copies of this cell bound it and the others had\n drifted, so the same column said different things on a detail page and in a grid. -->\n <input type=\"checkbox\"\n [checked]=\"display() === true\"\n [indeterminate]=\"isUnsetBoolean()\"\n disabled\n onclick=\"return false;\">\n} @else if (column().dataType === 'color') {\n @if (display(); as colorVal) {\n <!-- Inline on purpose, and it is not a style-guide slip. In a query grid this cell is\n rendered INSIDE <mp-datatable>'s shadow root, which does not see document\n stylesheets: neither this component's scoped rules nor Bootstrap utilities apply\n there. Inline styles do. Measured 2026-08-23 -- see the comment in the .scss. -->\n <span class=\"d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"\n style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n} @else if (chips().length) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of chips(); track chip.id) {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n </span>\n} @else if (link(); as route) {\n <a [routerLink]=\"route\">{{ display() }}</a>\n} @else {\n {{ display() }}\n}\n", styles: [".spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
337
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkGridCellComponent, isStandalone: true, selector: "spark-grid-cell", inputs: { column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, rendererValue: { classPropertyName: "rendererValue", publicName: "rendererValue", isSignal: true, isRequired: false, transformFunction: null }, item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: false, transformFunction: null }, chips: { classPropertyName: "chips", publicName: "chips", isSignal: true, isRequired: false, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (rendererComponent(); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: rendererInputs()\"></ng-container>\n} @else if (column().dataType === 'boolean') {\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked box, which reads\n as an explicit `false`. One of the three copies of this cell bound it and the others had\n drifted, so the same column said different things on a detail page and in a grid. -->\n <input type=\"checkbox\"\n [checked]=\"display() === true\"\n [indeterminate]=\"isUnsetBoolean()\"\n disabled\n onclick=\"return false;\">\n} @else if (column().dataType === 'color') {\n @if (display(); as colorVal) {\n <!-- Inline on purpose, and it is not a style-guide slip. In a query grid this cell is\n rendered INSIDE <mp-datatable>'s shadow root, which does not see document\n stylesheets: neither this component's scoped rules nor Bootstrap utilities apply\n there. Inline styles do. Measured 2026-08-23 -- see the comment in the .scss. -->\n <span class=\"d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"\n style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n} @else if (column().dataType === 'image') {\n @if (display(); as src) {\n <!-- Inline sizing for the same reason the colour swatch above uses it: in a query grid this\n renders inside <mp-datatable>'s shadow root, which sees neither this component's scoped\n rules nor Bootstrap's. `alt` is empty on purpose \u2014 the cell is decorative next to the\n row's own labelled columns, and a filename read aloud is worse than nothing. -->\n <img [src]=\"src\" alt=\"\" loading=\"lazy\" class=\"align-middle rounded\"\n style=\"max-height: 2.5em; max-width: 100%; object-fit: contain;\">\n }\n} @else if (column().dataType === 'url') {\n @if (display(); as href) {\n <!-- rel is not optional with target=_blank: without noopener the opened page gets a handle on\n this one through window.opener, and these hrefs are data. -->\n <a [href]=\"href\" target=\"_blank\" rel=\"noopener noreferrer\">{{ href }}</a>\n }\n} @else if (chips().length) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of chips(); track chip.id) {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n </span>\n} @else if (link(); as route) {\n <a [routerLink]=\"route\">{{ display() }}</a>\n} @else {\n {{ display() }}\n}\n", styles: [".spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
342
338
|
}
|
|
343
339
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkGridCellComponent, decorators: [{
|
|
344
340
|
type: Component,
|
|
345
|
-
args: [{ selector: 'spark-grid-cell', imports: [CommonModule, NgComponentOutlet, RouterModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (rendererComponent(); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: rendererInputs()\"></ng-container>\n} @else if (column().dataType === 'boolean') {\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked box, which reads\n as an explicit `false`. One of the three copies of this cell bound it and the others had\n drifted, so the same column said different things on a detail page and in a grid. -->\n <input type=\"checkbox\"\n [checked]=\"display() === true\"\n [indeterminate]=\"isUnsetBoolean()\"\n disabled\n onclick=\"return false;\">\n} @else if (column().dataType === 'color') {\n @if (display(); as colorVal) {\n <!-- Inline on purpose, and it is not a style-guide slip. In a query grid this cell is\n rendered INSIDE <mp-datatable>'s shadow root, which does not see document\n stylesheets: neither this component's scoped rules nor Bootstrap utilities apply\n there. Inline styles do. Measured 2026-08-23 -- see the comment in the .scss. -->\n <span class=\"d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"\n style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n} @else if (chips().length) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of chips(); track chip.id) {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n </span>\n} @else if (link(); as route) {\n <a [routerLink]=\"route\">{{ display() }}</a>\n} @else {\n {{ display() }}\n}\n", styles: [".spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}\n"] }]
|
|
341
|
+
args: [{ selector: 'spark-grid-cell', imports: [CommonModule, NgComponentOutlet, RouterModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (rendererComponent(); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: rendererInputs()\"></ng-container>\n} @else if (column().dataType === 'boolean') {\n <!-- [indeterminate] matters: without it a null boolean renders as an unchecked box, which reads\n as an explicit `false`. One of the three copies of this cell bound it and the others had\n drifted, so the same column said different things on a detail page and in a grid. -->\n <input type=\"checkbox\"\n [checked]=\"display() === true\"\n [indeterminate]=\"isUnsetBoolean()\"\n disabled\n onclick=\"return false;\">\n} @else if (column().dataType === 'color') {\n @if (display(); as colorVal) {\n <!-- Inline on purpose, and it is not a style-guide slip. In a query grid this cell is\n rendered INSIDE <mp-datatable>'s shadow root, which does not see document\n stylesheets: neither this component's scoped rules nor Bootstrap utilities apply\n there. Inline styles do. Measured 2026-08-23 -- see the comment in the .scss. -->\n <span class=\"d-inline-block align-middle border rounded\"\n [style.background-color]=\"colorVal\"\n style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n} @else if (column().dataType === 'image') {\n @if (display(); as src) {\n <!-- Inline sizing for the same reason the colour swatch above uses it: in a query grid this\n renders inside <mp-datatable>'s shadow root, which sees neither this component's scoped\n rules nor Bootstrap's. `alt` is empty on purpose \u2014 the cell is decorative next to the\n row's own labelled columns, and a filename read aloud is worse than nothing. -->\n <img [src]=\"src\" alt=\"\" loading=\"lazy\" class=\"align-middle rounded\"\n style=\"max-height: 2.5em; max-width: 100%; object-fit: contain;\">\n }\n} @else if (column().dataType === 'url') {\n @if (display(); as href) {\n <!-- rel is not optional with target=_blank: without noopener the opened page gets a handle on\n this one through window.opener, and these hrefs are data. -->\n <a [href]=\"href\" target=\"_blank\" rel=\"noopener noreferrer\">{{ href }}</a>\n }\n} @else if (chips().length) {\n <span class=\"d-inline-flex flex-wrap gap-1\">\n @for (chip of chips(); track chip.id) {\n <span class=\"spark-chip\">{{ chip.label }}</span>\n }\n </span>\n} @else if (link(); as route) {\n <a [routerLink]=\"route\">{{ display() }}</a>\n} @else {\n {{ display() }}\n}\n", styles: [".spark-chip{display:inline-block;padding:.35em .9em;font-size:.8125em;font-weight:600;line-height:1.4;white-space:nowrap;text-align:center;vertical-align:baseline;color:var(--bs-body-color);background-color:var(--bs-secondary-bg);border:1px solid var(--bs-border-color);border-radius:50rem;text-decoration:none}a.spark-chip:hover,a.spark-chip:focus-visible{background-color:var(--bs-tertiary-bg)}.spark-color-swatch{width:1.5em;height:1.5em}\n"] }]
|
|
346
342
|
}], propDecorators: { column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], rendererValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "rendererValue", required: false }] }], item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: false }] }], chips: [{ type: i0.Input, args: [{ isSignal: true, alias: "chips", required: false }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }] } });
|
|
347
343
|
|
|
348
344
|
/**
|
|
@@ -415,6 +411,12 @@ class SparkQueryGridComponent {
|
|
|
415
411
|
*/
|
|
416
412
|
search = input('', /* @ts-ignore */
|
|
417
413
|
...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
|
|
414
|
+
/**
|
|
415
|
+
* Column metadata for externally supplied rows. Required whenever `data` is bound, because a
|
|
416
|
+
* projection cannot describe itself — there is no per-row metadata left to infer it from.
|
|
417
|
+
*/
|
|
418
|
+
columns = input(null, /* @ts-ignore */
|
|
419
|
+
...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
418
420
|
/**
|
|
419
421
|
* Change this to re-run the query. Any value works; only its identity matters.
|
|
420
422
|
*
|
|
@@ -441,11 +443,49 @@ class SparkQueryGridComponent {
|
|
|
441
443
|
/** Rows the user has ticked. Two-way so chrome outside the grid can read and clear them. */
|
|
442
444
|
selection = model([], /* @ts-ignore */
|
|
443
445
|
...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
|
|
446
|
+
/**
|
|
447
|
+
* Replaces where the first column's link points, per row.
|
|
448
|
+
*
|
|
449
|
+
* Return `null` to suppress the link for that row — useful when a grid mixes rows that have a
|
|
450
|
+
* detail page with rows that do not.
|
|
451
|
+
*
|
|
452
|
+
* ⚠️ **It changes the destination, not the permission.** `canRead()` still gates whether any
|
|
453
|
+
* link is rendered at all, and this function is never consulted when that gate is closed. It
|
|
454
|
+
* cannot be used to reach a row the rights model withheld; it exists for the case where the
|
|
455
|
+
* row's natural destination is not `/po/{type}/{id}` — a composed row that maps to a page of
|
|
456
|
+
* its own, or a grid embedded in a route that owns its own child paths.
|
|
457
|
+
*/
|
|
458
|
+
rowRoute = input(null, /* @ts-ignore */
|
|
459
|
+
...(ngDevMode ? [{ debugName: "rowRoute" }] : /* istanbul ignore next */ []));
|
|
444
460
|
/** Emitted whenever a load or a page fetch fails, for a host in bespoke chrome. */
|
|
445
461
|
error = output();
|
|
446
462
|
rowClicked = output();
|
|
447
463
|
customActionExecuted = output();
|
|
448
464
|
colors = Color;
|
|
465
|
+
/**
|
|
466
|
+
* Where the first column's link points for this row: the host's `rowRoute` when it supplied one,
|
|
467
|
+
* otherwise the row's own detail page.
|
|
468
|
+
*
|
|
469
|
+
* A method rather than a computed because the answer is per row, and the template already
|
|
470
|
+
* iterates rows — a computed would have to be a map keyed by id, rebuilt on every page.
|
|
471
|
+
*/
|
|
472
|
+
rowRouteFor(row) {
|
|
473
|
+
const custom = this.rowRoute();
|
|
474
|
+
if (custom)
|
|
475
|
+
return custom(row);
|
|
476
|
+
const type = this.entityType();
|
|
477
|
+
// ⚠️ A composed type has no per-row detail page by construction: its rows are computed, and
|
|
478
|
+
// `{Name}Actions.OnLoadAsync` serves ONE page which is free to ignore the id it was given. So
|
|
479
|
+
// /po/{type}/{rowId} does not 404 — it silently renders that same page, which is worse, because
|
|
480
|
+
// nothing looks wrong. This branch is live in DemoApp today: Read/StartPage implies
|
|
481
|
+
// Query/StartPage, so the composed grid renders with canRead() true and every row linked.
|
|
482
|
+
//
|
|
483
|
+
// `clrType` is absent exactly for those types and is already on the wire; the grid simply never
|
|
484
|
+
// consulted it. A host whose composed rows DO have a page says so with `rowRoute`.
|
|
485
|
+
if (!type?.clrType)
|
|
486
|
+
return null;
|
|
487
|
+
return ['/po', type.alias || type.id, row.id];
|
|
488
|
+
}
|
|
449
489
|
query = signal(null, /* @ts-ignore */
|
|
450
490
|
...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
|
|
451
491
|
entityType = signal(null, /* @ts-ignore */
|
|
@@ -481,8 +521,24 @@ class SparkQueryGridComponent {
|
|
|
481
521
|
...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
|
|
482
522
|
isVirtualScrolling = computed(() => isVirtualScrollingQuery(this.query()), /* @ts-ignore */
|
|
483
523
|
...(ngDevMode ? [{ debugName: "isVirtualScrolling" }] : /* istanbul ignore next */ []));
|
|
484
|
-
|
|
485
|
-
|
|
524
|
+
/**
|
|
525
|
+
* Columns as sent with the most recent fetched page.
|
|
526
|
+
*
|
|
527
|
+
* A host binding `data` (streaming) supplies its own via the `columns` input instead — the
|
|
528
|
+
* snapshot carries them once, exactly as a paged result does.
|
|
529
|
+
*/
|
|
530
|
+
fetchedColumns = signal([], /* @ts-ignore */
|
|
531
|
+
...(ngDevMode ? [{ debugName: "fetchedColumns" }] : /* istanbul ignore next */ []));
|
|
532
|
+
/** Every column on the wire, drawn or not — what renderers and lookups resolve against. */
|
|
533
|
+
allColumns = computed(() => this.columns() ?? this.fetchedColumns(), /* @ts-ignore */
|
|
534
|
+
...(ngDevMode ? [{ debugName: "allColumns" }] : /* istanbul ignore next */ []));
|
|
535
|
+
/**
|
|
536
|
+
* The columns the grid draws. `isVisible: false` ships a value without a column, so a renderer
|
|
537
|
+
* can read a sibling the grid does not show; undefined means visible, so a server that predates
|
|
538
|
+
* the field still draws everything.
|
|
539
|
+
*/
|
|
540
|
+
visibleColumns = computed(() => this.allColumns().filter(c => c.isVisible !== false), /* @ts-ignore */
|
|
541
|
+
...(ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []));
|
|
486
542
|
/**
|
|
487
543
|
* True when rows do not come from this component's own fetch.
|
|
488
544
|
*
|
|
@@ -496,7 +552,7 @@ class SparkQueryGridComponent {
|
|
|
496
552
|
...(ngDevMode ? [{ debugName: "hasExternalData" }] : /* istanbul ignore next */ []));
|
|
497
553
|
/** Whether an action's selection rule is satisfied right now. The server checks it again. */
|
|
498
554
|
isActionEnabled(action) {
|
|
499
|
-
return parseSelectionRule(action.selectionRule)(this.selection().length);
|
|
555
|
+
return parseSelectionRule(action.selectionRule)(this.selection().map(r => r.id).length);
|
|
500
556
|
}
|
|
501
557
|
constructor() {
|
|
502
558
|
effect(() => {
|
|
@@ -574,7 +630,19 @@ class SparkQueryGridComponent {
|
|
|
574
630
|
return;
|
|
575
631
|
}
|
|
576
632
|
try {
|
|
577
|
-
|
|
633
|
+
// The sub-query's container travels with the action (#327). Without it an action invoked
|
|
634
|
+
// from a company's Cars tab could not tell it was on a company's page at all — the grid knew
|
|
635
|
+
// (it filters the rows by that parent) and simply dropped the fact on the way out.
|
|
636
|
+
//
|
|
637
|
+
// Sent as parentId/parentType, NOT as the `parent` argument: that one means an object of this
|
|
638
|
+
// action's OWN type and is loaded under it server-side, so handing it a Company id for a Car
|
|
639
|
+
// action refuses — correctly, and confusingly.
|
|
640
|
+
const pId = this.parentId();
|
|
641
|
+
const pType = this.parentType();
|
|
642
|
+
await this.sparkService.executeCustomAction(this.entityType().id, action.name, undefined, this.selection().map(r => r.id), pId && pType ? { id: pId, type: pType } : undefined,
|
|
643
|
+
// The query too: the server re-runs it narrowed to these ids, so the action receives the
|
|
644
|
+
// rows this grid rendered rather than a re-derivation from documents.
|
|
645
|
+
this.query()?.id);
|
|
578
646
|
this.customActionExecuted.emit({ action });
|
|
579
647
|
if (action.refreshOnCompleted)
|
|
580
648
|
this.reload();
|
|
@@ -674,7 +742,7 @@ class SparkQueryGridComponent {
|
|
|
674
742
|
}
|
|
675
743
|
const sourceName = extractSourceName(query.source);
|
|
676
744
|
const singular = singularize(sourceName);
|
|
677
|
-
return entityTypes.find(t => t.name === sourceName || t.name === singular || t.clrType
|
|
745
|
+
return entityTypes.find(t => t.name === sourceName || t.name === singular || t.clrType?.endsWith(singular)) ?? null;
|
|
678
746
|
}
|
|
679
747
|
makeFetch(query, parentId, parentType) {
|
|
680
748
|
return (req) => this.sparkService.executeQuery(query.id, {
|
|
@@ -685,11 +753,16 @@ class SparkQueryGridComponent {
|
|
|
685
753
|
parentId, parentType,
|
|
686
754
|
}).then(r => {
|
|
687
755
|
this.errorMessage.set(null);
|
|
688
|
-
this.resultCount.set(r.
|
|
756
|
+
this.resultCount.set(r.totalItems);
|
|
757
|
+
// Columns come from the result now, not from the entity type: the server decides the query
|
|
758
|
+
// surface (ShowedOn.Query) and is the same place the sort-column allow-list is checked.
|
|
759
|
+
// ?? [] because a malformed or older response must render an empty grid, not throw inside a
|
|
760
|
+
// computed — where the stack points at the column filter and not at the response that lacked them.
|
|
761
|
+
this.fetchedColumns.set(r.columns ?? []);
|
|
689
762
|
return {
|
|
690
|
-
data: r.
|
|
691
|
-
totalRecords: r.
|
|
692
|
-
totalPages: Math.ceil(r.
|
|
763
|
+
data: r.items,
|
|
764
|
+
totalRecords: r.totalItems,
|
|
765
|
+
totalPages: Math.ceil(r.totalItems / req.perPage) || 1,
|
|
693
766
|
perPage: req.perPage,
|
|
694
767
|
page: req.page,
|
|
695
768
|
};
|
|
@@ -702,30 +775,33 @@ class SparkQueryGridComponent {
|
|
|
702
775
|
});
|
|
703
776
|
}
|
|
704
777
|
async loadLookupReferenceOptions() {
|
|
705
|
-
|
|
778
|
+
// Over ALL columns, not just the drawn ones: an undrawn column's value can still be rendered
|
|
779
|
+
// by a renderer reading it as a sibling, and it would resolve to a raw key without its options.
|
|
780
|
+
this.lookupReferenceOptions.set(await this.gridRenderers.loadLookupOptions(this.allColumns()));
|
|
706
781
|
}
|
|
707
782
|
/**
|
|
708
783
|
* The underlying value a custom renderer receives, which is not the printable one.
|
|
709
784
|
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
785
|
+
* A projection is flat, so this is the cell's own value rather than a display string. It is
|
|
786
|
+
* deliberately not the attribute-shaped `rendererValue`: a query row has no nested object to
|
|
787
|
+
* fall back to, and reaching for one would hand every renderer `undefined`.
|
|
712
788
|
*/
|
|
713
|
-
rendererValueFor(item,
|
|
714
|
-
return
|
|
789
|
+
rendererValueFor(item, column) {
|
|
790
|
+
return cellValue(valueFor(item, column.name));
|
|
715
791
|
}
|
|
716
|
-
getColumnRendererComponent(
|
|
717
|
-
return this.gridRenderers.columnComponentFor(
|
|
792
|
+
getColumnRendererComponent(column) {
|
|
793
|
+
return this.gridRenderers.columnComponentFor(column);
|
|
718
794
|
}
|
|
719
|
-
getColumnRendererInputs(component, item,
|
|
720
|
-
return this.gridRenderers.columnInputsFor(component, item,
|
|
795
|
+
getColumnRendererInputs(component, item, column) {
|
|
796
|
+
return this.gridRenderers.columnInputsFor(component, item, column);
|
|
721
797
|
}
|
|
722
798
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
723
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkQueryGridComponent, isStandalone: true, selector: "spark-query-grid", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: false, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, reloadToken: { classPropertyName: "reloadToken", publicName: "reloadToken", isSignal: true, isRequired: false, transformFunction: null }, settings: { classPropertyName: "settings", publicName: "settings", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { settings: "settingsChange", selection: "selectionChange", error: "error", rowClicked: "rowClicked", customActionExecuted: "customActionExecuted" }, ngImport: i0, 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 \u2014 no card, no message, nothing to\n see or report \u2014 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 \u2014 binding both makes `[fetch]` win silently \u2014 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 <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [(settings)]=\"settings\">\n @for (
|
|
799
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkQueryGridComponent, isStandalone: true, selector: "spark-query-grid", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: false, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, reloadToken: { classPropertyName: "reloadToken", publicName: "reloadToken", isSignal: true, isRequired: false, transformFunction: null }, settings: { classPropertyName: "settings", publicName: "settings", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, rowRoute: { classPropertyName: "rowRoute", publicName: "rowRoute", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { settings: "settingsChange", selection: "selectionChange", error: "error", rowClicked: "rowClicked", customActionExecuted: "customActionExecuted" }, ngImport: i0, 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 \u2014 no card, no message, nothing to\n see or report \u2014 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 \u2014 binding both makes `[fetch]` win silently \u2014 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 <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [(settings)]=\"settings\">\n @for (col of visibleColumns(); track col.name) {\n <div *bsDatatableColumn=\"col.name; sortable: true\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\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 \u2014 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 \u2014 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\"\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 </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", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "announce", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["columns", "data", "fetch", "settings", "selectionMode", "selectable", "selection", "rowKey", "resizableColumns", "pagination", "virtualScroll", "itemSize", "virtualBuffer", "isResponsive", "compareWith", "tree", "idKey", "childCountKey", "treeIndent", "expandedIds", "selectionStrategy"], outputs: ["settingsChange", "selectionChange", "rowClick", "rowDblClick", "rowContextMenu", "expandedIdsChange", "rowExpand", "rowCollapse"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkGridCellComponent, selector: "spark-grid-cell", inputs: ["column", "display", "rendererValue", "item", "chips", "link"] }, { kind: "component", type: SparkAttributeDescriptionComponent, selector: "spark-attribute-description", inputs: ["description", "position"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: QueryCellValuePipe, name: "queryCellValue" }, { kind: "pipe", type: QueryReferenceChipsPipe, name: "queryReferenceChips" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
724
800
|
}
|
|
725
801
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryGridComponent, decorators: [{
|
|
726
802
|
type: Component,
|
|
727
|
-
args: [{ selector: 'spark-query-grid', imports: [CommonModule, RouterModule, BsAlertComponent, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsSpinnerComponent, SparkGridCellComponent, ResolveTranslationPipe,
|
|
728
|
-
}], ctorParameters: () => [], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: false }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], reloadToken: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadToken", required: false }] }], settings: [{ type: i0.Input, args: [{ isSignal: true, alias: "settings", required: false }] }, { type: i0.Output, args: ["settingsChange"] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], error: [{ type: i0.Output, args: ["error"] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }] } });
|
|
803
|
+
args: [{ selector: 'spark-query-grid', imports: [CommonModule, RouterModule, BsAlertComponent, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsSpinnerComponent, SparkGridCellComponent, ResolveTranslationPipe, QueryCellValuePipe, QueryReferenceChipsPipe, TranslateKeyPipe, SparkAttributeDescriptionComponent], changeDetection: ChangeDetectionStrategy.OnPush, 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 \u2014 no card, no message, nothing to\n see or report \u2014 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 \u2014 binding both makes `[fetch]` win silently \u2014 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 <bs-datatable\n [selectionMode]=\"selectionMode()\"\n [(selection)]=\"selection\"\n [virtualScroll]=\"isVirtualScrolling()\"\n [itemSize]=\"40\"\n [isResponsive]=\"isVirtualScrolling()\"\n [data]=\"data()\"\n [fetch]=\"fetchFn()\"\n [(settings)]=\"settings\">\n @for (col of visibleColumns(); track col.name) {\n <div *bsDatatableColumn=\"col.name; sortable: true\">\n {{ (col.label | resolveTranslation) || col.name }}\n <spark-attribute-description [description]=\"col.description\" />\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 \u2014 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 \u2014 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\"\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 </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" }]
|
|
804
|
+
}], ctorParameters: () => [], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: false }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], reloadToken: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadToken", required: false }] }], settings: [{ type: i0.Input, args: [{ isSignal: true, alias: "settings", required: false }] }, { type: i0.Output, args: ["settingsChange"] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], rowRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowRoute", required: false }] }], error: [{ type: i0.Output, args: ["error"] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }] } });
|
|
729
805
|
function extractSourceName(source) {
|
|
730
806
|
const dotIndex = source.indexOf('.');
|
|
731
807
|
return dotIndex >= 0 ? source.substring(dotIndex + 1) : source;
|
|
@@ -804,6 +880,12 @@ class SparkQueryCardComponent {
|
|
|
804
880
|
error = output();
|
|
805
881
|
rowClicked = output();
|
|
806
882
|
customActionExecuted = output();
|
|
883
|
+
/**
|
|
884
|
+
* Forwarded to the grid. Without this the escape hatch was unreachable from a card — i.e. from
|
|
885
|
+
* every auto-rendered sub-query and program-unit query page, which is most grids.
|
|
886
|
+
*/
|
|
887
|
+
rowRoute = input(null, /* @ts-ignore */
|
|
888
|
+
...(ngDevMode ? [{ debugName: "rowRoute" }] : /* istanbul ignore next */ []));
|
|
807
889
|
colors = Color;
|
|
808
890
|
iconSlots = contentChildren(SparkQueryIconDirective, /* @ts-ignore */
|
|
809
891
|
...(ngDevMode ? [{ debugName: "iconSlots" }] : /* istanbul ignore next */ []));
|
|
@@ -845,12 +927,12 @@ class SparkQueryCardComponent {
|
|
|
845
927
|
selection = computed(() => this.grid()?.selection() ?? [], /* @ts-ignore */
|
|
846
928
|
...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
|
|
847
929
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
848
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkQueryCardComponent, isStandalone: true, selector: "spark-query-card", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: false, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: false, transformFunction: null }, reloadToken: { classPropertyName: "reloadToken", publicName: "reloadToken", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, iconTemplate: { classPropertyName: "iconTemplate", publicName: "iconTemplate", isSignal: true, isRequired: false, transformFunction: null }, captionTemplate: { classPropertyName: "captionTemplate", publicName: "captionTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { error: "error", rowClicked: "rowClicked", customActionExecuted: "customActionExecuted" }, queries: [{ propertyName: "iconSlots", predicate: SparkQueryIconDirective, isSignal: true }, { propertyName: "captionSlots", predicate: SparkQueryCaptionDirective, isSignal: true }, { propertyName: "actionSlots", predicate: SparkQueryActionsDirective, isSignal: true }], viewQueries: [{ propertyName: "grid", first: true, predicate: SparkQueryGridComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly \u2014 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
|
|
930
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SparkQueryCardComponent, isStandalone: true, selector: "spark-query-card", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: false, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: false, transformFunction: null }, reloadToken: { classPropertyName: "reloadToken", publicName: "reloadToken", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, iconTemplate: { classPropertyName: "iconTemplate", publicName: "iconTemplate", isSignal: true, isRequired: false, transformFunction: null }, captionTemplate: { classPropertyName: "captionTemplate", publicName: "captionTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, rowRoute: { classPropertyName: "rowRoute", publicName: "rowRoute", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { error: "error", rowClicked: "rowClicked", customActionExecuted: "customActionExecuted" }, queries: [{ propertyName: "iconSlots", predicate: SparkQueryIconDirective, isSignal: true }, { propertyName: "captionSlots", predicate: SparkQueryCaptionDirective, isSignal: true }, { propertyName: "actionSlots", predicate: SparkQueryActionsDirective, isSignal: true }], viewQueries: [{ propertyName: "grid", first: true, predicate: SparkQueryGridComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly \u2014 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 @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (customActions().length) {\n <!-- The default: the actions the server declares for this type. The nav renders only when\n there are any, so a query without actions is pixel-identical to a plain caption.\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 rule. -->\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of customActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" class=\"btn btn-sm btn-outline-primary\"\n [disabled]=\"!grid()?.isActionEnabled(action)\" (click)=\"grid()?.onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n }\n\n <!--\n Caption last, actions first. The auto margin is conditional rather than fixed, and that is\n the point: with actions present `ms-auto` pushes the caption to the trailing edge, and with\n none it falls back to `me-auto` so a plain card looks exactly as it always did. A fixed\n `ms-auto` would have right-aligned the caption of every action-less query card in every app\n \u2014 a change nobody asked for, to fix a layout only some cards have.\n -->\n <span [class.ms-auto]=\"customActions().length\" [class.me-auto]=\"!customActions().length\">\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 </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]=\"search()\"\n [rowRoute]=\"rowRoute()\"\n (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["color", "outline"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["color", "navStyle"] }, { kind: "component", type: BsPriorityNavComponent, selector: "bs-priority-nav", inputs: ["moreLabel", "moreLabelTemplate", "collapseAt", "overflowFrom", "hideEmptyMore", "ariaLabel"], outputs: ["overflowChange"] }, { kind: "directive", type: BsPriorityNavItemDirective, selector: "[bsPriorityNavItem]", inputs: ["bsPriorityNavItem", "bsPriorityNavItemHideBelow"] }, { kind: "component", type: SparkQueryGridComponent, selector: "spark-query-grid", inputs: ["queryId", "parentId", "parentType", "data", "search", "columns", "reloadToken", "settings", "selection", "rowRoute"], outputs: ["settingsChange", "selectionChange", "error", "rowClicked", "customActionExecuted"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
849
931
|
}
|
|
850
932
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SparkQueryCardComponent, decorators: [{
|
|
851
933
|
type: Component,
|
|
852
|
-
args: [{ selector: 'spark-query-card', imports: [CommonModule, BsCardComponent, BsCardHeaderComponent, BsPriorityNavComponent, BsPriorityNavItemDirective, SparkQueryGridComponent, ResolveTranslationPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly \u2014 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
|
|
853
|
-
}], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: false }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: false }] }], reloadToken: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadToken", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], iconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTemplate", required: false }] }], captionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionTemplate", required: false }] }], actionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionsTemplate", required: false }] }], error: [{ type: i0.Output, args: ["error"] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }], iconSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryIconDirective), { isSignal: true }] }], captionSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryCaptionDirective), { isSignal: true }] }], actionSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryActionsDirective), { isSignal: true }] }], grid: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SparkQueryGridComponent), { isSignal: true }] }] } });
|
|
934
|
+
args: [{ selector: 'spark-query-card', imports: [CommonModule, BsCardComponent, BsCardHeaderComponent, BsPriorityNavComponent, BsPriorityNavItemDirective, SparkQueryGridComponent, ResolveTranslationPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n The card is always rendered. A host that wants no card uses <spark-query-grid> directly \u2014 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 @if (actionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl; context: { $implicit: customActions(), selection: selection(), query: query() }\"></ng-container>\n } @else if (customActions().length) {\n <!-- The default: the actions the server declares for this type. The nav renders only when\n there are any, so a query without actions is pixel-identical to a plain caption.\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 rule. -->\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n @for (action of customActions(); track action.name) {\n <button *bsPriorityNavItem=\"10 + action.offset\" class=\"btn btn-sm btn-outline-primary\"\n [disabled]=\"!grid()?.isActionEnabled(action)\" (click)=\"grid()?.onCustomAction(action)\">\n {{ action.displayName | resolveTranslation }}\n </button>\n }\n </bs-priority-nav>\n }\n\n <!--\n Caption last, actions first. The auto margin is conditional rather than fixed, and that is\n the point: with actions present `ms-auto` pushes the caption to the trailing edge, and with\n none it falls back to `me-auto` so a plain card looks exactly as it always did. A fixed\n `ms-auto` would have right-aligned the caption of every action-less query card in every app\n \u2014 a change nobody asked for, to fix a layout only some cards have.\n -->\n <span [class.ms-auto]=\"customActions().length\" [class.me-auto]=\"!customActions().length\">\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 </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]=\"search()\"\n [rowRoute]=\"rowRoute()\"\n (error)=\"error.emit($event)\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n </div>\n</bs-card>\n" }]
|
|
935
|
+
}], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: false }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: false }] }], reloadToken: [{ type: i0.Input, args: [{ isSignal: true, alias: "reloadToken", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], iconTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTemplate", required: false }] }], captionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionTemplate", required: false }] }], actionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionsTemplate", required: false }] }], error: [{ type: i0.Output, args: ["error"] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], customActionExecuted: [{ type: i0.Output, args: ["customActionExecuted"] }], rowRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowRoute", required: false }] }], iconSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryIconDirective), { isSignal: true }] }], captionSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryCaptionDirective), { isSignal: true }] }], actionSlots: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SparkQueryActionsDirective), { isSignal: true }] }], grid: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SparkQueryGridComponent), { isSignal: true }] }] } });
|
|
854
936
|
|
|
855
937
|
/**
|
|
856
938
|
* Generated bundle index. Do not edit.
|