@mintplayer/ng-spark 22.24.0 → 22.25.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 +15 -0
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs +5 -3
- package/fesm2022/mintplayer-ng-spark-client-operations.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-contributions.mjs +721 -0
- package/fesm2022/mintplayer-ng-spark-contributions.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-spark-grid.mjs +131 -4
- package/fesm2022/mintplayer-ng-spark-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-history.mjs +1 -14
- package/fesm2022/mintplayer-ng-spark-history.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-models.mjs +41 -2
- package/fesm2022/mintplayer-ng-spark-models.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-moderation.mjs +4 -4
- package/fesm2022/mintplayer-ng-spark-moderation.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-panels.mjs +9 -1
- package/fesm2022/mintplayer-ng-spark-panels.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-pipes.mjs +39 -3
- package/fesm2022/mintplayer-ng-spark-pipes.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs +15 -6
- package/fesm2022/mintplayer-ng-spark-po-detail.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs +924 -16
- package/fesm2022/mintplayer-ng-spark-po-edit.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-po-form.mjs +78 -20
- package/fesm2022/mintplayer-ng-spark-po-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-query-list.mjs +60 -5
- package/fesm2022/mintplayer-ng-spark-query-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs +28 -1
- package/fesm2022/mintplayer-ng-spark-renderers.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark-shell.mjs +31 -13
- package/fesm2022/mintplayer-ng-spark-shell.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-spark.mjs +4 -1
- package/fesm2022/mintplayer-ng-spark.mjs.map +1 -1
- package/package.json +7 -2
- package/types/mintplayer-ng-spark-client-operations.d.ts +7 -1
- package/types/mintplayer-ng-spark-contributions.d.ts +231 -0
- package/types/mintplayer-ng-spark-grid.d.ts +35 -3
- package/types/mintplayer-ng-spark-history.d.ts +3 -10
- package/types/mintplayer-ng-spark-models.d.ts +60 -3
- package/types/mintplayer-ng-spark-panels.d.ts +36 -1
- package/types/mintplayer-ng-spark-pipes.d.ts +12 -1
- package/types/mintplayer-ng-spark-po-detail.d.ts +4 -1
- package/types/mintplayer-ng-spark-po-edit.d.ts +305 -1
- package/types/mintplayer-ng-spark-po-form.d.ts +64 -15
- package/types/mintplayer-ng-spark-query-list.d.ts +26 -1
- package/types/mintplayer-ng-spark-renderers.d.ts +44 -2
- package/types/mintplayer-ng-spark-shell.d.ts +24 -9
- package/types/mintplayer-ng-spark.d.ts +12 -0
|
@@ -103,7 +103,10 @@ var SparkQueryListComponent = class SparkQueryListComponent {
|
|
|
103
103
|
this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => {
|
|
104
104
|
this.onParamsChange(params).catch((e) => this.reportLoadFailure(e));
|
|
105
105
|
});
|
|
106
|
-
this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((query) =>
|
|
106
|
+
this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((query) => {
|
|
107
|
+
this.deletedMode.set(parseSparkDeletedParam(query.get("deleted")) ?? "exclude");
|
|
108
|
+
this.applyUrlScope(query);
|
|
109
|
+
});
|
|
107
110
|
this.destroyRef.onDestroy(() => this.disconnectStreaming());
|
|
108
111
|
effect(() => {
|
|
109
112
|
const q = this.query();
|
|
@@ -117,6 +120,57 @@ var SparkQueryListComponent = class SparkQueryListComponent {
|
|
|
117
120
|
if (this.isStreaming()) this.applyFilter();
|
|
118
121
|
});
|
|
119
122
|
}
|
|
123
|
+
urlParentId = signal("", ...ngDevMode ? [{ debugName: "urlParentId" }] : /* istanbul ignore next */ []);
|
|
124
|
+
urlParentType = signal("", ...ngDevMode ? [{ debugName: "urlParentType" }] : /* istanbul ignore next */ []);
|
|
125
|
+
urlFilters = signal([], ...ngDevMode ? [{ debugName: "urlFilters" }] : /* istanbul ignore next */ []);
|
|
126
|
+
activeUrlFilters = computed(() => this.grid()?.appliedPresetFilters() ?? [], ...ngDevMode ? [{ debugName: "activeUrlFilters" }] : /* istanbul ignore next */ []);
|
|
127
|
+
static reservedParams = /* @__PURE__ */ new Set([
|
|
128
|
+
"deleted",
|
|
129
|
+
"parentId",
|
|
130
|
+
"parentType",
|
|
131
|
+
"queryId",
|
|
132
|
+
"parentDeleted"
|
|
133
|
+
]);
|
|
134
|
+
applyUrlScope(query) {
|
|
135
|
+
const parentId = query.get("parentId") ?? "";
|
|
136
|
+
const parentType = query.get("parentType") ?? "";
|
|
137
|
+
const both = !!parentId && !!parentType;
|
|
138
|
+
if (this.urlParentId() !== (both ? parentId : "")) this.urlParentId.set(both ? parentId : "");
|
|
139
|
+
if (this.urlParentType() !== (both ? parentType : "")) this.urlParentType.set(both ? parentType : "");
|
|
140
|
+
const filters = query.keys.filter((key) => !SparkQueryListComponent.reservedParams.has(key)).map((key) => ({
|
|
141
|
+
name: key,
|
|
142
|
+
includes: query.getAll(key).filter((v) => v !== "")
|
|
143
|
+
})).filter((f) => f.includes.length > 0);
|
|
144
|
+
if (JSON.stringify(filters) !== JSON.stringify(this.urlFilters())) this.urlFilters.set(filters);
|
|
145
|
+
}
|
|
146
|
+
clearUrlFilter(name) {
|
|
147
|
+
this.router.navigate([], {
|
|
148
|
+
relativeTo: this.route,
|
|
149
|
+
queryParams: { [name]: null },
|
|
150
|
+
queryParamsHandling: "merge",
|
|
151
|
+
replaceUrl: true
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
clearUrlScope() {
|
|
155
|
+
const queryParams = {
|
|
156
|
+
parentId: null,
|
|
157
|
+
parentType: null
|
|
158
|
+
};
|
|
159
|
+
for (const filter of this.urlFilters()) queryParams[filter.name] = null;
|
|
160
|
+
this.router.navigate([], {
|
|
161
|
+
relativeTo: this.route,
|
|
162
|
+
queryParams,
|
|
163
|
+
queryParamsHandling: "merge",
|
|
164
|
+
replaceUrl: true
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
filterValues(filter) {
|
|
168
|
+
return (filter.includes ?? []).map((v) => String(v)).join(", ");
|
|
169
|
+
}
|
|
170
|
+
filterLabel(name) {
|
|
171
|
+
const attribute = this.entityType()?.attributes?.find((a) => a.name === name);
|
|
172
|
+
return this.lang.resolve(attribute?.label) || name;
|
|
173
|
+
}
|
|
120
174
|
async onParamsChange(params) {
|
|
121
175
|
this.errorMessage.set(null);
|
|
122
176
|
this.queryId.set(null);
|
|
@@ -273,8 +327,8 @@ var SparkQueryListComponent = class SparkQueryListComponent {
|
|
|
273
327
|
isSignal: true
|
|
274
328
|
}],
|
|
275
329
|
ngImport: i0,
|
|
276
|
-
template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
|
|
277
|
-
styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--bs-
|
|
330
|
+
template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
|
|
331
|
+
styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--spark-shell-main-bg, var(--bs-body-bg));border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 calc(-1 * var(--spark-main-padding, 1.5rem)) calc(-1 * var(--spark-main-padding, 1.5rem));--spark-content-inset-inline: 0px}:host.virtual-scrolling spark-query-grid{display:flex;flex-direction:column;min-height:0}:host.virtual-scrolling ::ng-deep bs-datatable{display:flex;flex-direction:column;min-height:0;--mp-datatable-virtual-max-height: 100%}:host.virtual-scrolling ::ng-deep bs-datatable mp-datatable{flex:1 1 auto;min-height:0}tr:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.05)}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}.spark-live-badge{font-size:.6em}.spark-url-filter-clear{background:none;border:0;padding:0 0 0 .25rem;color:inherit;line-height:1}\n"],
|
|
278
332
|
dependencies: [
|
|
279
333
|
{
|
|
280
334
|
kind: "component",
|
|
@@ -390,6 +444,7 @@ var SparkQueryListComponent = class SparkQueryListComponent {
|
|
|
390
444
|
"queryId",
|
|
391
445
|
"parentId",
|
|
392
446
|
"parentType",
|
|
447
|
+
"presetFilters",
|
|
393
448
|
"data",
|
|
394
449
|
"search",
|
|
395
450
|
"columns",
|
|
@@ -464,8 +519,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
464
519
|
],
|
|
465
520
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
466
521
|
host: { "[class.virtual-scrolling]": "isVirtualScrolling()" },
|
|
467
|
-
template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
|
|
468
|
-
styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--bs-
|
|
522
|
+
template: "<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n",
|
|
523
|
+
styles: [".spark-actionbar{--_bleed-block: var(--spark-content-inset-block, 0px);--_bleed-inline: var(--spark-content-inset-inline, 0px);--_gutter: var(--spark-actionbar-gutter, 1rem);position:sticky;top:calc(-1 * var(--_bleed-block));z-index:400;background-color:var(--spark-shell-main-bg, var(--bs-body-bg));border-bottom:1px solid var(--bs-border-color);margin-bottom:1rem;margin-inline:calc(-1 * var(--_bleed-inline));padding-inline:calc(var(--_bleed-inline) + var(--_gutter))}.spark-actionbar .btn{border-radius:0}.spark-actionbar-wide{--spark-actionbar-gutter: 1.5rem}:host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 calc(-1 * var(--spark-main-padding, 1.5rem)) calc(-1 * var(--spark-main-padding, 1.5rem));--spark-content-inset-inline: 0px}:host.virtual-scrolling spark-query-grid{display:flex;flex-direction:column;min-height:0}:host.virtual-scrolling ::ng-deep bs-datatable{display:flex;flex-direction:column;min-height:0;--mp-datatable-virtual-max-height: 100%}:host.virtual-scrolling ::ng-deep bs-datatable mp-datatable{flex:1 1 auto;min-height:0}tr:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.05)}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}.spark-live-badge{font-size:.6em}.spark-url-filter-clear{background:none;border:0;padding:0 0 0 .25rem;color:inherit;line-height:1}\n"]
|
|
469
524
|
}]
|
|
470
525
|
}],
|
|
471
526
|
ctorParameters: () => [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-query-list.mjs","names":[],"sources":["../../query-list/src/spark-query-list.component.ts","../../query-list/src/spark-query-list.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, viewChild, TemplateRef } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { Subscription } from 'rxjs';\nimport { CommonModule, NgTemplateOutlet } from '@angular/common';\nimport { ActivatedRoute, ParamMap, Router } from '@angular/router';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsBadgeComponent } from '@mintplayer/ng-bootstrap/badge';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';\nimport { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { SparkService, SparkStreamingService, SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport { TranslateKeyPipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkQueryGridComponent, SparkQueryToolbarAction, SparkSearchBoxComponent, sparkActionClass } from '@mintplayer/ng-spark/grid';\nimport {\n CustomActionDefinition,\n StreamingMessage,\n QueryColumn,\n QueryResultItem,\n SparkDeletedFilter,\n} from '@mintplayer/ng-spark/models';\nimport { NgComponentOutlet } from '@angular/common';\nimport {\n SPARK_QUERY_LIST_ACTIONS,\n SparkQueryListContext,\n orderSparkExtensions,\n parseSparkDeletedParam,\n} from '@mintplayer/ng-spark/panels';\n\n/**\n * The routed query page: chrome around one {@link SparkQueryGridComponent}.\n *\n * It owns what is genuinely page-shaped and route-shaped, and nothing else:\n *\n * - **Route resolution.** It has no `queryId` input; it reads `paramMap`, and it serves two\n * routes — `query/:queryId`, and `po/:type`, which resolves an entity type to a query. That\n * second one is type-to-query resolution, not query rendering, and is why this component still\n * exists rather than the router pointing at the grid.\n * - **Streaming.** The websocket lives here so it stays out of every PO detail page's bundle;\n * the snapshot is filtered and sorted client-side and handed to the grid as `[data]`.\n * - The action bar, the caption, the LIVE badge, the search box and the New button.\n *\n * The grid itself — columns, cells, paging, the row link, selection, custom-action execution —\n * is the shared component. This page previously wrote out `<bs-datatable>` twice, once per\n * transport, with a shared row template between them; both are gone.\n */\n@Component({\n selector: 'spark-query-list',\n imports: [BsBadgeComponent, CommonModule, NgTemplateOutlet, NgComponentOutlet, BsAlertComponent, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsPriorityNavComponent, BsPriorityNavItemDirective, BsSpinnerComponent, SparkIconComponent, SparkQueryGridComponent, SparkSearchBoxComponent, ResolveTranslationPipe, TranslateKeyPipe],\n templateUrl: './spark-query-list.component.html',\n styleUrl: './spark-query-list.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class.virtual-scrolling]': 'isVirtualScrolling()'\n }\n})\nexport class SparkQueryListComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n private readonly streamingService = inject(SparkStreamingService);\n private readonly destroyRef = inject(DestroyRef);\n protected readonly lang = inject(SparkLanguageService);\n\n extraActionsTemplate = input<TemplateRef<void> | null>(null);\n showCustomActions = input(true);\n\n /**\n * Forwarded to the grid, so a query PAGE can replace its row links — the same reason the card\n * forwards it. Without this the escape hatch existed only for a directly-embedded grid.\n */\n rowRoute = input<((row: QueryResultItem) => unknown[] | null) | null>(null);\n\n rowClicked = output<QueryResultItem>();\n createClicked = output<void>();\n customActionExecuted = output<{ action: CustomActionDefinition }>();\n\n colors = Color;\n\n /** The query the grid should render, resolved from the route. Null until it is known. */\n queryId = signal<string | null>(null);\n errorMessage = signal<string | null>(null);\n searchTerm = signal('');\n\n private readonly grid = viewChild(SparkQueryGridComponent);\n\n /**\n * Grid state, surfaced for this page's chrome.\n *\n * Optional `viewChild`, read defensively: the action bar and caption render above the grid, so\n * on the first change-detection pass the query has not resolved yet.\n */\n protected readonly query = computed(() => this.grid()?.query() ?? null);\n protected readonly entityType = computed(() => this.grid()?.entityType() ?? null);\n protected readonly customActions = computed(() => this.grid()?.customActions() ?? []);\n protected readonly canCreate = computed(() => this.grid()?.offersCreate() ?? false);\n protected readonly resultCount = computed(() => this.grid()?.resultCount() ?? null);\n protected readonly isVirtualScrolling = computed(() => this.grid()?.isVirtualScrolling() ?? false);\n protected readonly gridError = computed(() => this.grid()?.errorMessage() ?? null);\n protected readonly permissions = computed(() => this.grid()?.permissions() ?? null);\n\n /** Add-on buttons for the action bar (#460, `SPARK_QUERY_LIST_ACTIONS`), e.g. the Deleted toggle. */\n protected readonly listActions = orderSparkExtensions(inject(SPARK_QUERY_LIST_ACTIONS, { optional: true }), a => a.priority ?? 60);\n\n /**\n * The soft-deletion mode (#460, T2), read from the route's `?deleted=` so the recycle bin survives\n * a reload and the back button from a row opened in it. `exclude` unless the route says otherwise.\n */\n protected readonly deletedMode = signal<SparkDeletedFilter>('exclude');\n\n /** Bumped by `context.reload()`; the grid treats any new value as \"re-run the query\". */\n private readonly reloadToken = signal(0);\n\n protected readonly listContext = computed((): SparkQueryListContext | null => {\n const query = this.query();\n if (!query) return null;\n return {\n query,\n entityType: this.entityType(),\n permissions: this.permissions(),\n deleted: this.deletedMode(),\n setDeleted: (mode: SparkDeletedFilter) => this.setDeleted(mode),\n reload: () => this.reloadToken.update(n => n + 1),\n };\n });\n\n protected readonly gridReloadToken = this.reloadToken.asReadonly();\n\n private setDeleted(mode: SparkDeletedFilter): void {\n this.router.navigate([], {\n relativeTo: this.route,\n queryParams: { deleted: mode === 'exclude' ? null : mode },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n });\n }\n\n /** Whether an action's selection rule is satisfied. Delegated: the grid holds the selection. */\n /** @see SparkPoDetailComponent.customActionClass — same allow-list, same default. */\n protected customActionClass(action: CustomActionDefinition): string {\n const variant = action.variant?.toLowerCase();\n switch (variant) {\n case 'danger':\n case 'warning':\n case 'primary':\n case 'secondary':\n case 'success':\n return `btn btn-${variant}`;\n default:\n return 'btn btn-outline-primary';\n }\n }\n\n /** The grid's toolbar model (#460, M15), the same list the sub-query card renders. */\n protected readonly toolbarActions = computed(() => this.grid()?.toolbarActions() ?? []);\n\n protected toolbarActionClass(action: SparkQueryToolbarAction): string {\n // New keeps its solid primary look; the rest follow their variant.\n return action.kind === 'new' ? 'btn btn-primary' : sparkActionClass(action.definition);\n }\n\n protected isToolbarActionEnabled(action: SparkQueryToolbarAction): boolean {\n return this.grid()?.isToolbarActionEnabled(action) ?? false;\n }\n\n protected runToolbarAction(action: SparkQueryToolbarAction): void {\n if (action.kind === 'new') {\n // Through this page's own onCreate, so `createClicked` fires exactly as it always has.\n this.onCreate();\n return;\n }\n void this.grid()?.runToolbarAction(action);\n }\n\n protected isActionEnabled(action: CustomActionDefinition): boolean {\n return this.grid()?.isActionEnabled(action) ?? false;\n }\n\n // --- streaming ---------------------------------------------------------------\n\n isStreaming = signal(false);\n private streamingSub: Subscription | null = null;\n /** Columns as sent with the stream's snapshot; empty until it arrives. */\n protected readonly streamColumns = signal<QueryColumn[]>([]);\n private readonly allItems = signal<QueryResultItem[]>([]);\n private readonly streamItems = signal<QueryResultItem[]>([]);\n\n /**\n * Rows handed to the grid, or `null` to let it fetch for itself.\n *\n * Null for a normal query — an empty array would read as \"here are no rows\" and suppress the\n * fetch entirely.\n */\n protected readonly gridData = computed(() =>\n this.query()?.isStreamingQuery ? this.streamItems() : null);\n\n constructor() {\n this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => {\n // The handler is async and this is a subscribe, so a rejection lands nowhere: the metadata\n // load would reject, the query would stay null, and the template would render a spinner\n // FOREVER — while this component has had an errorMessage surface all along that only the\n // fetch path ever reached.\n this.onParamsChange(params).catch((e: unknown) => this.reportLoadFailure(e as HttpErrorResponse));\n });\n\n // Separate from paramMap: switching the recycle bin on changes only the query string, and must\n // not re-resolve the query (which would reset page, sort and filters for nothing).\n this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe(query =>\n this.deletedMode.set(parseSparkDeletedParam(query.get('deleted')) ?? 'exclude'));\n\n this.destroyRef.onDestroy(() => this.disconnectStreaming());\n\n // Connect the socket once the grid has resolved a streaming query, and disconnect whenever it\n // resolves anything else. The grid knows not to fetch for a streaming query, so there is no\n // window in which both transports are live.\n effect(() => {\n const q = this.query();\n if (q?.isStreamingQuery) {\n this.connectStreaming(q.id);\n } else {\n this.disconnectStreaming();\n }\n });\n\n // Client-side filter and sort for the streaming snapshot. The server never sees these: there\n // is no request to attach them to.\n effect(() => {\n this.searchTerm();\n this.grid()?.settings();\n this.allItems();\n if (this.isStreaming()) this.applyFilter();\n });\n }\n\n private async onParamsChange(params: ParamMap): Promise<void> {\n this.errorMessage.set(null);\n this.queryId.set(null);\n this.allItems.set([]);\n this.streamItems.set([]);\n this.streamColumns.set([]);\n this.disconnectStreaming();\n\n const queryId = params.get('queryId');\n const typeParam = params.get('type');\n\n if (queryId) {\n this.queryId.set(queryId);\n return;\n }\n\n if (!typeParam) return;\n\n // `po/:type` — the route names an entity type, so find the query that lists it. The grid takes\n // a query, and this translation is the only reason it cannot take the route directly.\n const [entityTypes, queries] = await Promise.all([\n this.sparkService.getEntityTypes(),\n this.sparkService.getQueries(),\n ]);\n const entityType = entityTypes.find(t => t.id === typeParam || t.alias === typeParam);\n if (!entityType) {\n this.reportLoadFailure({ status: 404 } as HttpErrorResponse);\n return;\n }\n\n const singularName = entityType.name;\n const match = queries.find(q => {\n if (q.entityType === singularName) return true;\n const sourceName = q.source.includes('.') ? q.source.substring(q.source.indexOf('.') + 1) : q.source;\n return sourceName === singularName || sourceName === singularName + 's';\n });\n\n if (match) this.queryId.set(match.alias || match.id);\n else this.reportLoadFailure({ status: 404 } as HttpErrorResponse);\n }\n\n /**\n * A load failure has to render, not just be swallowed: a denied query answers 404 (audit M-3, so\n * existence is not leaked), which is indistinguishable from a missing one — hence a deliberately\n * generic message rather than a guess at which it was.\n */\n private reportLoadFailure(err: HttpErrorResponse): void {\n this.errorMessage.set(\n err?.status === 404\n ? (this.lang.t('spark.query.unavailable') || 'This list is not available.')\n : (err?.error?.error || err?.message || 'An unexpected error occurred'));\n }\n\n protected async onCustomAction(action: CustomActionDefinition): Promise<void> {\n await this.grid()?.onCustomAction(action);\n }\n\n protected onCreate(): void {\n this.createClicked.emit();\n const et = this.entityType();\n if (et) this.router.navigate(['/po', et.alias || et.id, 'new']);\n }\n\n protected clearSearch(): void {\n this.searchTerm.set('');\n }\n\n private connectStreaming(queryId: string): void {\n if (this.streamingSub) return;\n this.isStreaming.set(true);\n\n this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({\n next: (message) => this.handleStreamingMessage(message),\n error: (err) => {\n this.errorMessage.set(err?.message || 'Streaming connection failed');\n this.isStreaming.set(false);\n },\n complete: () => this.isStreaming.set(false),\n });\n }\n\n private disconnectStreaming(): void {\n if (this.streamingSub) {\n this.streamingSub.unsubscribe();\n this.streamingSub = null;\n }\n this.isStreaming.set(false);\n }\n\n private handleStreamingMessage(message: StreamingMessage): void {\n switch (message.type) {\n case 'snapshot':\n this.errorMessage.set(null);\n // Columns come with the snapshot and never change for the life of the stream, so they are\n // stored once and handed to the grid alongside the rows — a projection cannot describe\n // itself, and the grid has no entity-type metadata to fall back on any more.\n this.streamColumns.set(message.columns);\n this.allItems.set(message.data);\n break;\n\n case 'patch':\n if (message.updated.length > 0) {\n this.allItems.update(items => items.map(item => {\n const patch = message.updated.find(u => u.id === item.id);\n if (!patch) return item;\n return {\n ...item,\n values: item.values.map(v =>\n v.key in patch.values ? { ...v, value: patch.values[v.key] } : v),\n };\n }));\n }\n break;\n\n case 'error':\n this.errorMessage.set(message.message);\n break;\n }\n }\n\n private applyFilter(): void {\n let items = this.allItems();\n\n const term = this.searchTerm().toLowerCase();\n if (term) {\n items = items.filter(item =>\n item.values.some(v => String(v.value ?? '').toLowerCase().includes(term)));\n }\n\n // The datatable in `[data]` mode also sorts on header clicks, but the sort must survive a\n // patch: re-deriving from `allItems` without re-applying it would silently reorder the grid\n // under the user on every update.\n const sortCols = this.grid()?.settings().sortColumns ?? [];\n if (sortCols.length > 0) {\n items = [...items].sort((a, b) => {\n for (const col of sortCols) {\n const aVal = a.values.find(v => v.key === col.property)?.value ?? '';\n const bVal = b.values.find(v => v.key === col.property)?.value ?? '';\n const cmp = String(aVal).localeCompare(String(bVal));\n if (cmp !== 0) return col.direction === 'descending' ? -cmp : cmp;\n }\n return 0;\n });\n }\n\n this.streamItems.set(items);\n }\n}\n","<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-query-list.mjs","names":[],"sources":["../../query-list/src/spark-query-list.component.ts","../../query-list/src/spark-query-list.component.html"],"sourcesContent":["import { ChangeDetectionStrategy, Component, DestroyRef, computed, effect, inject, input, output, signal, viewChild, TemplateRef } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { Subscription } from 'rxjs';\nimport { CommonModule, NgTemplateOutlet } from '@angular/common';\nimport { ActivatedRoute, ParamMap, Router } from '@angular/router';\nimport { Color } from '@mintplayer/ng-bootstrap';\nimport { BsBadgeComponent } from '@mintplayer/ng-bootstrap/badge';\nimport { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';\nimport { BsGridComponent, BsGridRowDirective, BsGridColumnDirective } from '@mintplayer/ng-bootstrap/grid';\nimport { BsPriorityNavComponent, BsPriorityNavItemDirective } from '@mintplayer/ng-bootstrap/priority-nav';\nimport { BsSpinnerComponent } from '@mintplayer/ng-bootstrap/spinner';\nimport { HttpErrorResponse } from '@angular/common/http';\nimport { SparkService, SparkStreamingService, SparkLanguageService } from '@mintplayer/ng-spark/services';\nimport { TranslateKeyPipe, ResolveTranslationPipe } from '@mintplayer/ng-spark/pipes';\nimport { SparkIconComponent } from '@mintplayer/ng-spark/icon';\nimport { SparkQueryGridComponent, SparkQueryToolbarAction, SparkSearchBoxComponent, sparkActionClass } from '@mintplayer/ng-spark/grid';\nimport {\n CustomActionDefinition,\n StreamingMessage,\n QueryColumn,\n QueryColumnFilter,\n QueryResultItem,\n SparkDeletedFilter,\n} from '@mintplayer/ng-spark/models';\nimport { NgComponentOutlet } from '@angular/common';\nimport {\n SPARK_QUERY_LIST_ACTIONS,\n SparkQueryListContext,\n orderSparkExtensions,\n parseSparkDeletedParam,\n} from '@mintplayer/ng-spark/panels';\n\n/**\n * The routed query page: chrome around one {@link SparkQueryGridComponent}.\n *\n * It owns what is genuinely page-shaped and route-shaped, and nothing else:\n *\n * - **Route resolution.** It has no `queryId` input; it reads `paramMap`, and it serves two\n * routes — `query/:queryId`, and `po/:type`, which resolves an entity type to a query. That\n * second one is type-to-query resolution, not query rendering, and is why this component still\n * exists rather than the router pointing at the grid.\n * - **Streaming.** The websocket lives here so it stays out of every PO detail page's bundle;\n * the snapshot is filtered and sorted client-side and handed to the grid as `[data]`.\n * - The action bar, the caption, the LIVE badge, the search box and the New button.\n *\n * The grid itself — columns, cells, paging, the row link, selection, custom-action execution —\n * is the shared component. This page previously wrote out `<bs-datatable>` twice, once per\n * transport, with a shared row template between them; both are gone.\n */\n@Component({\n selector: 'spark-query-list',\n imports: [BsBadgeComponent, CommonModule, NgTemplateOutlet, NgComponentOutlet, BsAlertComponent, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsPriorityNavComponent, BsPriorityNavItemDirective, BsSpinnerComponent, SparkIconComponent, SparkQueryGridComponent, SparkSearchBoxComponent, ResolveTranslationPipe, TranslateKeyPipe],\n templateUrl: './spark-query-list.component.html',\n styleUrl: './spark-query-list.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class.virtual-scrolling]': 'isVirtualScrolling()'\n }\n})\nexport class SparkQueryListComponent {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly sparkService = inject(SparkService);\n private readonly streamingService = inject(SparkStreamingService);\n private readonly destroyRef = inject(DestroyRef);\n protected readonly lang = inject(SparkLanguageService);\n\n extraActionsTemplate = input<TemplateRef<void> | null>(null);\n showCustomActions = input(true);\n\n /**\n * Forwarded to the grid, so a query PAGE can replace its row links — the same reason the card\n * forwards it. Without this the escape hatch existed only for a directly-embedded grid.\n */\n rowRoute = input<((row: QueryResultItem) => unknown[] | null) | null>(null);\n\n rowClicked = output<QueryResultItem>();\n createClicked = output<void>();\n customActionExecuted = output<{ action: CustomActionDefinition }>();\n\n colors = Color;\n\n /** The query the grid should render, resolved from the route. Null until it is known. */\n queryId = signal<string | null>(null);\n errorMessage = signal<string | null>(null);\n searchTerm = signal('');\n\n private readonly grid = viewChild(SparkQueryGridComponent);\n\n /**\n * Grid state, surfaced for this page's chrome.\n *\n * Optional `viewChild`, read defensively: the action bar and caption render above the grid, so\n * on the first change-detection pass the query has not resolved yet.\n */\n protected readonly query = computed(() => this.grid()?.query() ?? null);\n protected readonly entityType = computed(() => this.grid()?.entityType() ?? null);\n protected readonly customActions = computed(() => this.grid()?.customActions() ?? []);\n protected readonly canCreate = computed(() => this.grid()?.offersCreate() ?? false);\n protected readonly resultCount = computed(() => this.grid()?.resultCount() ?? null);\n protected readonly isVirtualScrolling = computed(() => this.grid()?.isVirtualScrolling() ?? false);\n protected readonly gridError = computed(() => this.grid()?.errorMessage() ?? null);\n protected readonly permissions = computed(() => this.grid()?.permissions() ?? null);\n\n /** Add-on buttons for the action bar (#460, `SPARK_QUERY_LIST_ACTIONS`), e.g. the Deleted toggle. */\n protected readonly listActions = orderSparkExtensions(inject(SPARK_QUERY_LIST_ACTIONS, { optional: true }), a => a.priority ?? 60);\n\n /**\n * The soft-deletion mode (#460, T2), read from the route's `?deleted=` so the recycle bin survives\n * a reload and the back button from a row opened in it. `exclude` unless the route says otherwise.\n */\n protected readonly deletedMode = signal<SparkDeletedFilter>('exclude');\n\n /** Bumped by `context.reload()`; the grid treats any new value as \"re-run the query\". */\n private readonly reloadToken = signal(0);\n\n protected readonly listContext = computed((): SparkQueryListContext | null => {\n const query = this.query();\n if (!query) return null;\n return {\n query,\n entityType: this.entityType(),\n permissions: this.permissions(),\n deleted: this.deletedMode(),\n setDeleted: (mode: SparkDeletedFilter) => this.setDeleted(mode),\n reload: () => this.reloadToken.update(n => n + 1),\n };\n });\n\n protected readonly gridReloadToken = this.reloadToken.asReadonly();\n\n private setDeleted(mode: SparkDeletedFilter): void {\n this.router.navigate([], {\n relativeTo: this.route,\n queryParams: { deleted: mode === 'exclude' ? null : mode },\n queryParamsHandling: 'merge',\n replaceUrl: true,\n });\n }\n\n /** Whether an action's selection rule is satisfied. Delegated: the grid holds the selection. */\n /** @see SparkPoDetailComponent.customActionClass — same allow-list, same default. */\n protected customActionClass(action: CustomActionDefinition): string {\n const variant = action.variant?.toLowerCase();\n switch (variant) {\n case 'danger':\n case 'warning':\n case 'primary':\n case 'secondary':\n case 'success':\n return `btn btn-${variant}`;\n default:\n return 'btn btn-outline-primary';\n }\n }\n\n /** The grid's toolbar model (#460, M15), the same list the sub-query card renders. */\n protected readonly toolbarActions = computed(() => this.grid()?.toolbarActions() ?? []);\n\n protected toolbarActionClass(action: SparkQueryToolbarAction): string {\n // New keeps its solid primary look; the rest follow their variant.\n return action.kind === 'new' ? 'btn btn-primary' : sparkActionClass(action.definition);\n }\n\n protected isToolbarActionEnabled(action: SparkQueryToolbarAction): boolean {\n return this.grid()?.isToolbarActionEnabled(action) ?? false;\n }\n\n protected runToolbarAction(action: SparkQueryToolbarAction): void {\n if (action.kind === 'new') {\n // Through this page's own onCreate, so `createClicked` fires exactly as it always has.\n this.onCreate();\n return;\n }\n void this.grid()?.runToolbarAction(action);\n }\n\n protected isActionEnabled(action: CustomActionDefinition): boolean {\n return this.grid()?.isActionEnabled(action) ?? false;\n }\n\n // --- streaming ---------------------------------------------------------------\n\n isStreaming = signal(false);\n private streamingSub: Subscription | null = null;\n /** Columns as sent with the stream's snapshot; empty until it arrives. */\n protected readonly streamColumns = signal<QueryColumn[]>([]);\n private readonly allItems = signal<QueryResultItem[]>([]);\n private readonly streamItems = signal<QueryResultItem[]>([]);\n\n /**\n * Rows handed to the grid, or `null` to let it fetch for itself.\n *\n * Null for a normal query — an empty array would read as \"here are no rows\" and suppress the\n * fetch entirely.\n */\n protected readonly gridData = computed(() =>\n this.query()?.isStreamingQuery ? this.streamItems() : null);\n\n constructor() {\n this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => {\n // The handler is async and this is a subscribe, so a rejection lands nowhere: the metadata\n // load would reject, the query would stay null, and the template would render a spinner\n // FOREVER — while this component has had an errorMessage surface all along that only the\n // fetch path ever reached.\n this.onParamsChange(params).catch((e: unknown) => this.reportLoadFailure(e as HttpErrorResponse));\n });\n\n // Separate from paramMap: switching the recycle bin on changes only the query string, and must\n // not re-resolve the query (which would reset page, sort and filters for nothing).\n this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe(query => {\n this.deletedMode.set(parseSparkDeletedParam(query.get('deleted')) ?? 'exclude');\n this.applyUrlScope(query);\n });\n\n this.destroyRef.onDestroy(() => this.disconnectStreaming());\n\n // Connect the socket once the grid has resolved a streaming query, and disconnect whenever it\n // resolves anything else. The grid knows not to fetch for a streaming query, so there is no\n // window in which both transports are live.\n effect(() => {\n const q = this.query();\n if (q?.isStreamingQuery) {\n this.connectStreaming(q.id);\n } else {\n this.disconnectStreaming();\n }\n });\n\n // Client-side filter and sort for the streaming snapshot. The server never sees these: there\n // is no request to attach them to.\n effect(() => {\n this.searchTerm();\n this.grid()?.settings();\n this.allItems();\n if (this.isStreaming()) this.applyFilter();\n });\n }\n\n // --- URL scope: parent and column filters ----------------------------------------------------\n\n /**\n * The parent the URL scopes the query to (`?parentId=…&parentType=…`), sent exactly as a sub-query\n * sends its container. Both or neither: one without the other is ignored, as the server does.\n */\n protected readonly urlParentId = signal('');\n protected readonly urlParentType = signal('');\n\n /**\n * Column filters from the URL: every other query-string parameter, `?Language=en&Script=Latn`\n * (repeat a name for several values). The grid applies only those naming an attribute of the\n * query's type; the rest are ignored. This is the contributions History link's shape, and works for\n * any query.\n */\n protected readonly urlFilters = signal<QueryColumnFilter[]>([]);\n\n /** The URL filters the grid applied, as removable chips. */\n protected readonly activeUrlFilters = computed(() => this.grid()?.appliedPresetFilters() ?? []);\n\n /** Query-string names the page or the row links own, never column filters. */\n private static readonly reservedParams = new Set(['deleted', 'parentId', 'parentType', 'queryId', 'parentDeleted']);\n\n private applyUrlScope(query: ParamMap): void {\n const parentId = query.get('parentId') ?? '';\n const parentType = query.get('parentType') ?? '';\n const both = !!parentId && !!parentType;\n if (this.urlParentId() !== (both ? parentId : '')) this.urlParentId.set(both ? parentId : '');\n if (this.urlParentType() !== (both ? parentType : '')) this.urlParentType.set(both ? parentType : '');\n\n const filters: QueryColumnFilter[] = query.keys\n .filter(key => !SparkQueryListComponent.reservedParams.has(key))\n .map(key => ({ name: key, includes: query.getAll(key).filter(v => v !== '') }))\n .filter(f => f.includes.length > 0);\n // Only on a real change: a new array would refetch the grid for nothing (the Deleted toggle\n // also changes the query string).\n if (JSON.stringify(filters) !== JSON.stringify(this.urlFilters())) this.urlFilters.set(filters);\n }\n\n /** Drops one URL filter (its query-string parameter); the grid refetches. */\n protected clearUrlFilter(name: string): void {\n this.router.navigate([], { relativeTo: this.route, queryParams: { [name]: null }, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n /** Drops the parent and every URL filter. */\n protected clearUrlScope(): void {\n const queryParams: Record<string, null> = { parentId: null, parentType: null };\n for (const filter of this.urlFilters()) queryParams[filter.name] = null;\n this.router.navigate([], { relativeTo: this.route, queryParams, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n protected filterValues(filter: QueryColumnFilter): string {\n return (filter.includes ?? []).map(v => String(v)).join(', ');\n }\n\n /** The column's label for a chip: the query's own column label, else the attribute's, else its name. */\n protected filterLabel(name: string): string {\n const attribute = this.entityType()?.attributes?.find(a => a.name === name);\n return this.lang.resolve(attribute?.label) || name;\n }\n\n private async onParamsChange(params: ParamMap): Promise<void> {\n this.errorMessage.set(null);\n this.queryId.set(null);\n this.allItems.set([]);\n this.streamItems.set([]);\n this.streamColumns.set([]);\n this.disconnectStreaming();\n\n const queryId = params.get('queryId');\n const typeParam = params.get('type');\n\n if (queryId) {\n this.queryId.set(queryId);\n return;\n }\n\n if (!typeParam) return;\n\n // `po/:type` — the route names an entity type, so find the query that lists it. The grid takes\n // a query, and this translation is the only reason it cannot take the route directly.\n const [entityTypes, queries] = await Promise.all([\n this.sparkService.getEntityTypes(),\n this.sparkService.getQueries(),\n ]);\n const entityType = entityTypes.find(t => t.id === typeParam || t.alias === typeParam);\n if (!entityType) {\n this.reportLoadFailure({ status: 404 } as HttpErrorResponse);\n return;\n }\n\n const singularName = entityType.name;\n const match = queries.find(q => {\n if (q.entityType === singularName) return true;\n const sourceName = q.source.includes('.') ? q.source.substring(q.source.indexOf('.') + 1) : q.source;\n return sourceName === singularName || sourceName === singularName + 's';\n });\n\n if (match) this.queryId.set(match.alias || match.id);\n else this.reportLoadFailure({ status: 404 } as HttpErrorResponse);\n }\n\n /**\n * A load failure has to render, not just be swallowed: a denied query answers 404 (audit M-3, so\n * existence is not leaked), which is indistinguishable from a missing one — hence a deliberately\n * generic message rather than a guess at which it was.\n */\n private reportLoadFailure(err: HttpErrorResponse): void {\n this.errorMessage.set(\n err?.status === 404\n ? (this.lang.t('spark.query.unavailable') || 'This list is not available.')\n : (err?.error?.error || err?.message || 'An unexpected error occurred'));\n }\n\n protected async onCustomAction(action: CustomActionDefinition): Promise<void> {\n await this.grid()?.onCustomAction(action);\n }\n\n protected onCreate(): void {\n this.createClicked.emit();\n const et = this.entityType();\n if (et) this.router.navigate(['/po', et.alias || et.id, 'new']);\n }\n\n protected clearSearch(): void {\n this.searchTerm.set('');\n }\n\n private connectStreaming(queryId: string): void {\n if (this.streamingSub) return;\n this.isStreaming.set(true);\n\n this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({\n next: (message) => this.handleStreamingMessage(message),\n error: (err) => {\n this.errorMessage.set(err?.message || 'Streaming connection failed');\n this.isStreaming.set(false);\n },\n complete: () => this.isStreaming.set(false),\n });\n }\n\n private disconnectStreaming(): void {\n if (this.streamingSub) {\n this.streamingSub.unsubscribe();\n this.streamingSub = null;\n }\n this.isStreaming.set(false);\n }\n\n private handleStreamingMessage(message: StreamingMessage): void {\n switch (message.type) {\n case 'snapshot':\n this.errorMessage.set(null);\n // Columns come with the snapshot and never change for the life of the stream, so they are\n // stored once and handed to the grid alongside the rows — a projection cannot describe\n // itself, and the grid has no entity-type metadata to fall back on any more.\n this.streamColumns.set(message.columns);\n this.allItems.set(message.data);\n break;\n\n case 'patch':\n if (message.updated.length > 0) {\n this.allItems.update(items => items.map(item => {\n const patch = message.updated.find(u => u.id === item.id);\n if (!patch) return item;\n return {\n ...item,\n values: item.values.map(v =>\n v.key in patch.values ? { ...v, value: patch.values[v.key] } : v),\n };\n }));\n }\n break;\n\n case 'error':\n this.errorMessage.set(message.message);\n break;\n }\n }\n\n private applyFilter(): void {\n let items = this.allItems();\n\n const term = this.searchTerm().toLowerCase();\n if (term) {\n items = items.filter(item =>\n item.values.some(v => String(v.value ?? '').toLowerCase().includes(term)));\n }\n\n // The datatable in `[data]` mode also sorts on header clicks, but the sort must survive a\n // patch: re-deriving from `allItems` without re-applying it would silently reorder the grid\n // under the user on every update.\n const sortCols = this.grid()?.settings().sortColumns ?? [];\n if (sortCols.length > 0) {\n items = [...items].sort((a, b) => {\n for (const col of sortCols) {\n const aVal = a.values.find(v => v.key === col.property)?.value ?? '';\n const bVal = b.values.find(v => v.key === col.property)?.value ?? '';\n const cmp = String(aVal).localeCompare(String(bVal));\n if (cmp !== 0) return col.direction === 'descending' ? -cmp : cmp;\n }\n return 0;\n });\n }\n\n this.streamItems.set(items);\n }\n}\n","<div class=\"d-flex flex-column h-100\">\n <div class=\"spark-actionbar py-2 flex-shrink-0\" [class.spark-actionbar-wide]=\"isVirtualScrolling()\">\n <bs-priority-nav [moreLabel]=\"lang.t('common.more')\" [collapseAt]=\"'sm'\">\n <!-- The grid's toolbar model (#460, M15): New, Delete and the custom actions — the list the\n sub-query card renders too. Disabled, not hidden: hiding makes the affordance\n undiscoverable and reflows the priority nav on every selection change. The server\n enforces the same rules. -->\n @for (action of toolbarActions(); track action.name) {\n @if (action.kind !== 'custom' || showCustomActions()) {\n <button *bsPriorityNavItem=\"action.priority\" type=\"button\" [class]=\"toolbarActionClass(action)\"\n [attr.data-action]=\"action.name\"\n [disabled]=\"!isToolbarActionEnabled(action)\" (click)=\"runToolbarAction(action)\">\n @if (action.kind === 'new') {\n <spark-icon name=\"plus-lg\" /> {{ 'common.new' | t }}\n } @else {\n @if (action.definition.icon && action.kind === 'delete') {\n <spark-icon [name]=\"action.definition.icon\" />\n }\n {{ action.definition.displayName | resolveTranslation }}\n }\n </button>\n }\n }\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *bsPriorityNavItem=\"50\">\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n </ng-container>\n }\n <!-- Add-on actions (SPARK_QUERY_LIST_ACTIONS), e.g. the soft-delete Deleted toggle. -->\n @if (listContext(); as ctx) {\n @for (action of listActions; track action.id) {\n <ng-container *bsPriorityNavItem=\"action.priority ?? 60\">\n <ng-container *ngComponentOutlet=\"action.component; inputs: { context: ctx }\" />\n </ng-container>\n }\n }\n </bs-priority-nav>\n </div>\n\n <h2 class=\"mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n {{ (query()?.description | resolveTranslation) || query()?.name || ('common.loading' | t) }}\n @if (isStreaming()) {\n <bs-badge [type]=\"colors.success\" class=\"ms-2 align-middle spark-live-badge\">LIVE</bs-badge>\n }\n @if (deletedMode() === 'only') {\n <bs-badge [type]=\"colors.secondary\" class=\"ms-2 align-middle spark-deleted-badge\">{{ 'softDelete.deleted' | t }}</bs-badge>\n }\n </h2>\n\n @if (errorMessage(); as err) {\n <!-- The route could not be resolved to a query at all. A failure INSIDE the grid renders in\n the grid; this is the one it cannot report, because it never got a query id. -->\n <bs-alert [type]=\"colors.danger\" class=\"m-3 d-block\">\n {{ err }}\n </bs-alert>\n } @else if (queryId(); as qId) {\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"12\">\n <!-- The same box as the sub-query card's header (#460 M15): debounce, clear button. -->\n <spark-search-box [(term)]=\"searchTerm\" />\n </div>\n <!-- Its own full-width row rather than a column beside the box: the count is\n occasional, and reserving 8 columns for it shrank the search permanently. The\n @if moved out of the column so nothing reserves vertical space when absent. -->\n @if (searchTerm() && resultCount() !== null) {\n <div [md]=\"12\" class=\"text-end mt-1\">\n <span class=\"text-muted\">\n {{ resultCount() }} {{ resultCount() === 1 ? ('common.resultFound' | t) : ('common.resultsFound' | t) }}\n </span>\n </div>\n }\n </div>\n </bs-grid>\n </div>\n\n <!--\n One grid, both transports. `data` is null for a normal query, so the grid fetches and pages\n server-side; for a streaming query this page owns the socket and hands the filtered snapshot\n in. This used to be two near-identical <bs-datatable> blocks with a shared row template.\n -->\n <!-- The URL's scope (#contributions M5b): a parent and column filters from the query string, as\n the History link sends them. Shown so the narrowing is visible, and clearable. -->\n @if (urlParentId() || activeUrlFilters().length) {\n <div class=\"spark-url-filters d-flex flex-wrap align-items-center gap-2 mb-2 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <span class=\"text-muted small\">{{ 'common.filteredBy' | t }}</span>\n @for (filter of activeUrlFilters(); track filter.name) {\n <bs-badge [type]=\"colors.secondary\" class=\"spark-url-filter\" [attr.data-filter]=\"filter.name\">\n {{ filterLabel(filter.name) }}: {{ filterValues(filter) }}\n <button type=\"button\" class=\"spark-url-filter-clear\" [attr.aria-label]=\"'common.clearFilter' | t\"\n (click)=\"clearUrlFilter(filter.name)\">\n <spark-icon name=\"x-circle\" />\n </button>\n </bs-badge>\n }\n <button type=\"button\" class=\"btn btn-sm btn-link spark-url-filters-clear\" (click)=\"clearUrlScope()\">\n {{ 'common.clearFilters' | t }}\n </button>\n </div>\n }\n\n <spark-query-grid class=\"flex-grow-1\"\n [queryId]=\"qId\"\n [parentId]=\"urlParentId()\"\n [parentType]=\"urlParentType()\"\n [presetFilters]=\"urlFilters()\"\n [data]=\"gridData()\"\n [columns]=\"gridData() ? streamColumns() : null\"\n [search]=\"searchTerm()\"\n [deleted]=\"deletedMode()\"\n [reloadToken]=\"gridReloadToken()\"\n [rowRoute]=\"rowRoute()\"\n (rowClicked)=\"rowClicked.emit($event)\"\n (customActionExecuted)=\"customActionExecuted.emit($event)\" />\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n"],"mappings":";;;;;;;;;;;;;;;;;AA2DA,IAAa,0BAAb,MAAa,wBAAuB;CACjB,QAAQ,OAAO,cAAc;CAC7B,SAAS,OAAO,MAAM;CACtB,eAAe,OAAO,YAAY;CAClC,mBAAmB,OAAO,qBAAqB;CAC/C,aAAa,OAAO,UAAU;CAC5B,OAAO,OAAO,oBAAoB;CAErD,uBAAuB,MAAgC,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,uBAAA,CAAA,+BAAA,CAAA,CAAA;CAC5D,oBAAoB,MAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,oBAAA,CAAA,+BAAA,CAAA,CAAA;CAM/B,WAAW,MAA2D,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAE3E,aAAa,OAAM;CACnB,gBAAgB,OAAM;CACtB,uBAAuB,OAAM;CAE7B,SAAS;CAGT,UAAU,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CACrC,eAAe,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAC1C,aAAa,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEN,OAAO,UAAU,yBAAwB,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAQvC,QAAQ,eAAe,KAAK,KAAI,CAAE,EAAE,MAAK,KAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACpD,aAAa,eAAe,KAAK,KAAI,CAAE,EAAE,WAAU,KAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAC9D,gBAAgB,eAAe,KAAK,KAAI,CAAE,EAAE,cAAa,KAAM,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAClE,YAAY,eAAe,KAAK,KAAI,CAAE,EAAE,aAAY,KAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAChE,cAAc,eAAe,KAAK,KAAI,CAAE,EAAE,YAAW,KAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAChE,qBAAqB,eAAe,KAAK,KAAI,CAAE,EAAE,mBAAkB,KAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,qBAAA,CAAA,+BAAA,CAAA,CAAA;CAC/E,YAAY,eAAe,KAAK,KAAI,CAAE,EAAE,aAAY,KAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAC/D,cAAc,eAAe,KAAK,KAAI,CAAE,EAAE,YAAW,KAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAGhE,cAAc,qBAAqB,OAAO,0BAA0B,EAAE,UAAU,KAAI,CAAE,IAAG,MAAK,EAAE,YAAY,EAAE;CAM9G,cAAc,OAA2B,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAGrD,cAAc,OAAO,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAErB,cAAc,eAA4C;EAC3E,MAAM,QAAQ,KAAK,MAAK;EACxB,IAAI,CAAC,OAAO,OAAO;EACnB,OAAO;GACL;GACA,YAAY,KAAK,WAAU;GAC3B,aAAa,KAAK,YAAW;GAC7B,SAAS,KAAK,YAAW;GACzB,aAAa,SAA6B,KAAK,WAAW,IAAI;GAC9D,cAAc,KAAK,YAAY,QAAO,MAAK,IAAI,CAAC;EACjD;CACH,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,kBAAkB,KAAK,YAAY,WAAU;CAExD,WAAW,MAAwB;EACzC,KAAK,OAAO,SAAS,CAAA,GAAI;GACvB,YAAY,KAAK;GACjB,aAAa,EAAE,SAAS,SAAS,YAAY,OAAO,KAAI;GACxD,qBAAqB;GACrB,YAAY;EACb,CAAA;CACH;CAIU,kBAAkB,QAA8B;EACxD,MAAM,UAAU,OAAO,SAAS,YAAW;EAC3C,QAAQ,SAAR;GACE,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,WACH,OAAO,WAAW;GACpB,SACE,OAAO;EACX;CACF;CAGmB,iBAAiB,eAAe,KAAK,KAAI,CAAE,EAAE,eAAc,KAAM,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAE7E,mBAAmB,QAA+B;EAE1D,OAAO,OAAO,SAAS,QAAQ,oBAAoB,iBAAiB,OAAO,UAAU;CACvF;CAEU,uBAAuB,QAA+B;EAC9D,OAAO,KAAK,KAAI,CAAE,EAAE,uBAAuB,MAAM,KAAK;CACxD;CAEU,iBAAiB,QAA+B;EACxD,IAAI,OAAO,SAAS,OAAO;GAEzB,KAAK,SAAQ;GACb;EACF;EACA,AAAK,KAAK,KAAI,CAAE,EAAE,iBAAiB,MAAM;CAC3C;CAEU,gBAAgB,QAA8B;EACtD,OAAO,KAAK,KAAI,CAAE,EAAE,gBAAgB,MAAM,KAAK;CACjD;CAIA,cAAc,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CACnB,eAAoC;CAEzB,gBAAgB,OAAsB,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAC3C,WAAW,OAA0B,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CACxC,cAAc,OAA0B,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAQzC,WAAW,eAC5B,KAAK,MAAK,CAAE,EAAE,mBAAmB,KAAK,YAAW,IAAK,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAE7D,cAAA;EACE,KAAK,MAAM,SAAS,KAAK,mBAAkB,CAAE,CAAC,CAAC,WAAU,WAAS;GAKhE,KAAK,eAAe,MAAM,CAAC,CAAC,OAAO,MAAe,KAAK,kBAAkB,CAAsB,CAAC;EAClG,CAAC;EAID,KAAK,MAAM,cAAc,KAAK,mBAAkB,CAAE,CAAC,CAAC,WAAU,UAAQ;GACpE,KAAK,YAAY,IAAI,uBAAuB,MAAM,IAAI,SAAS,CAAC,KAAK,SAAS;GAC9E,KAAK,cAAc,KAAK;EAC1B,CAAC;EAED,KAAK,WAAW,gBAAgB,KAAK,oBAAmB,CAAE;EAK1D,aAAY;GACV,MAAM,IAAI,KAAK,MAAK;GACpB,IAAI,GAAG,kBACL,KAAK,iBAAiB,EAAE,EAAE;QAE1B,KAAK,oBAAmB;EAE5B,CAAC;EAID,aAAY;GACV,KAAK,WAAU;GACf,KAAK,KAAI,CAAE,EAAE,SAAQ;GACrB,KAAK,SAAQ;GACb,IAAI,KAAK,YAAW,GAAI,KAAK,YAAW;EAC1C,CAAC;CACH;CAQmB,cAAc,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CACxB,gBAAgB,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAQ1B,aAAa,OAA4B,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAG5C,mBAAmB,eAAe,KAAK,KAAI,CAAE,EAAE,qBAAoB,KAAM,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAGvF,OAAgB,iCAAiB,IAAI,IAAI;EAAC;EAAW;EAAY;EAAc;EAAW;CAAe,CAAC;CAE1G,cAAc,OAAe;EACnC,MAAM,WAAW,MAAM,IAAI,UAAU,KAAK;EAC1C,MAAM,aAAa,MAAM,IAAI,YAAY,KAAK;EAC9C,MAAM,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;EAC7B,IAAI,KAAK,YAAW,OAAQ,OAAO,WAAW,KAAK,KAAK,YAAY,IAAI,OAAO,WAAW,EAAE;EAC5F,IAAI,KAAK,cAAa,OAAQ,OAAO,aAAa,KAAK,KAAK,cAAc,IAAI,OAAO,aAAa,EAAE;EAEpG,MAAM,UAA+B,MAAM,KACxC,QAAO,QAAO,CAAC,wBAAwB,eAAe,IAAI,GAAG,CAAC,CAAC,CAC/D,KAAI,SAAQ;GAAE,MAAM;GAAK,UAAU,MAAM,OAAO,GAAG,CAAC,CAAC,QAAO,MAAK,MAAM,EAAE;EAAC,EAAG,CAAC,CAC9E,QAAO,MAAK,EAAE,SAAS,SAAS,CAAC;EAGpC,IAAI,KAAK,UAAU,OAAO,MAAM,KAAK,UAAU,KAAK,WAAU,CAAE,GAAG,KAAK,WAAW,IAAI,OAAO;CAChG;CAGU,eAAe,MAAY;EACnC,KAAK,OAAO,SAAS,CAAA,GAAI;GAAE,YAAY,KAAK;GAAO,aAAa,GAAG,OAAO,KAAI;GAAI,qBAAqB;GAAS,YAAY;EAAI,CAAE;CACpI;CAGU,gBAAa;EACrB,MAAM,cAAoC;GAAE,UAAU;GAAM,YAAY;EAAI;EAC5E,KAAK,MAAM,UAAU,KAAK,WAAU,GAAI,YAAY,OAAO,QAAQ;EACnE,KAAK,OAAO,SAAS,CAAA,GAAI;GAAE,YAAY,KAAK;GAAO;GAAa,qBAAqB;GAAS,YAAY;EAAI,CAAE;CAClH;CAEU,aAAa,QAAyB;EAC9C,QAAQ,OAAO,YAAY,CAAA,EAAA,CAAI,KAAI,MAAK,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;CAC9D;CAGU,YAAY,MAAY;EAChC,MAAM,YAAY,KAAK,WAAU,CAAE,EAAE,YAAY,MAAK,MAAK,EAAE,SAAS,IAAI;EAC1E,OAAO,KAAK,KAAK,QAAQ,WAAW,KAAK,KAAK;CAChD;CAEQ,MAAM,eAAe,QAAgB;EAC3C,KAAK,aAAa,IAAI,IAAI;EAC1B,KAAK,QAAQ,IAAI,IAAI;EACrB,KAAK,SAAS,IAAI,CAAA,CAAE;EACpB,KAAK,YAAY,IAAI,CAAA,CAAE;EACvB,KAAK,cAAc,IAAI,CAAA,CAAE;EACzB,KAAK,oBAAmB;EAExB,MAAM,UAAU,OAAO,IAAI,SAAS;EACpC,MAAM,YAAY,OAAO,IAAI,MAAM;EAEnC,IAAI,SAAS;GACX,KAAK,QAAQ,IAAI,OAAO;GACxB;EACF;EAEA,IAAI,CAAC,WAAW;EAIhB,MAAM,CAAC,aAAa,WAAW,MAAM,QAAQ,IAAI,CAC/C,KAAK,aAAa,eAAc,GAChC,KAAK,aAAa,WAAU,CAC7B,CAAA;EACD,MAAM,aAAa,YAAY,MAAK,MAAK,EAAE,OAAO,aAAa,EAAE,UAAU,SAAS;EACpF,IAAI,CAAC,YAAY;GACf,KAAK,kBAAkB,EAAE,QAAQ,IAAG,CAAuB;GAC3D;EACF;EAEA,MAAM,eAAe,WAAW;EAChC,MAAM,QAAQ,QAAQ,MAAK,MAAI;GAC7B,IAAI,EAAE,eAAe,cAAc,OAAO;GAC1C,MAAM,aAAa,EAAE,OAAO,SAAS,GAAG,IAAI,EAAE,OAAO,UAAU,EAAE,OAAO,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE;GAC9F,OAAO,eAAe,gBAAgB,eAAe,eAAe;EACtE,CAAC;EAED,IAAI,OAAO,KAAK,QAAQ,IAAI,MAAM,SAAS,MAAM,EAAE;OAC9C,KAAK,kBAAkB,EAAE,QAAQ,IAAG,CAAuB;CAClE;CAOQ,kBAAkB,KAAsB;EAC9C,KAAK,aAAa,IAChB,KAAK,WAAW,MACX,KAAK,KAAK,EAAE,yBAAyB,KAAK,gCAC1C,KAAK,OAAO,SAAS,KAAK,WAAW,8BAA+B;CAC7E;CAEU,MAAM,eAAe,QAA8B;EAC3D,MAAM,KAAK,KAAI,CAAE,EAAE,eAAe,MAAM;CAC1C;CAEU,WAAQ;EAChB,KAAK,cAAc,KAAI;EACvB,MAAM,KAAK,KAAK,WAAU;EAC1B,IAAI,IAAI,KAAK,OAAO,SAAS;GAAC;GAAO,GAAG,SAAS,GAAG;GAAI;EAAK,CAAC;CAChE;CAEU,cAAW;EACnB,KAAK,WAAW,IAAI,EAAE;CACxB;CAEQ,iBAAiB,SAAe;EACtC,IAAI,KAAK,cAAc;EACvB,KAAK,YAAY,IAAI,IAAI;EAEzB,KAAK,eAAe,KAAK,iBAAiB,wBAAwB,OAAO,CAAC,CAAC,UAAU;GACnF,OAAO,YAAY,KAAK,uBAAuB,OAAO;GACtD,QAAQ,QAAO;IACb,KAAK,aAAa,IAAI,KAAK,WAAW,6BAA6B;IACnE,KAAK,YAAY,IAAI,KAAK;GAC5B;GACA,gBAAgB,KAAK,YAAY,IAAI,KAAK;EAC3C,CAAA;CACH;CAEQ,sBAAmB;EACzB,IAAI,KAAK,cAAc;GACrB,KAAK,aAAa,YAAW;GAC7B,KAAK,eAAe;EACtB;EACA,KAAK,YAAY,IAAI,KAAK;CAC5B;CAEQ,uBAAuB,SAAyB;EACtD,QAAQ,QAAQ,MAAhB;GACE,KAAK;IACH,KAAK,aAAa,IAAI,IAAI;IAI1B,KAAK,cAAc,IAAI,QAAQ,OAAO;IACtC,KAAK,SAAS,IAAI,QAAQ,IAAI;IAC9B;GAEF,KAAK;IACH,IAAI,QAAQ,QAAQ,SAAS,GAC3B,KAAK,SAAS,QAAO,UAAS,MAAM,KAAI,SAAO;KAC7C,MAAM,QAAQ,QAAQ,QAAQ,MAAK,MAAK,EAAE,OAAO,KAAK,EAAE;KACxD,IAAI,CAAC,OAAO,OAAO;KACnB,OAAO;MACL,GAAG;MACH,QAAQ,KAAK,OAAO,KAAI,MACtB,EAAE,OAAO,MAAM,SAAS;OAAE,GAAG;OAAG,OAAO,MAAM,OAAO,EAAE;MAAI,IAAK,CAAC;KACnE;IACH,CAAC,CAAC;IAEJ;GAEF,KAAK,SACH,KAAK,aAAa,IAAI,QAAQ,OAAO;EAEzC;CACF;CAEQ,cAAW;EACjB,IAAI,QAAQ,KAAK,SAAQ;EAEzB,MAAM,OAAO,KAAK,WAAU,CAAE,CAAC,YAAW;EAC1C,IAAI,MACF,QAAQ,MAAM,QAAO,SACnB,KAAK,OAAO,MAAK,MAAK,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,YAAW,CAAE,CAAC,SAAS,IAAI,CAAC,CAAC;EAM7E,MAAM,WAAW,KAAK,KAAI,CAAE,EAAE,SAAQ,CAAE,CAAC,eAAe,CAAA;EACxD,IAAI,SAAS,SAAS,GACpB,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAK;GAC/B,KAAK,MAAM,OAAO,UAAU;IAC1B,MAAM,OAAO,EAAE,OAAO,MAAK,MAAK,EAAE,QAAQ,IAAI,QAAQ,CAAC,EAAE,SAAS;IAClE,MAAM,OAAO,EAAE,OAAO,MAAK,MAAK,EAAE,QAAQ,IAAI,QAAQ,CAAC,EAAE,SAAS;IAClE,MAAM,MAAM,OAAO,IAAI,CAAC,CAAC,cAAc,OAAO,IAAI,CAAC;IACnD,IAAI,QAAQ,GAAG,OAAO,IAAI,cAAc,eAAe,CAAC,MAAM;GAChE;GACA,OAAO;EACT,CAAC;EAGH,KAAK,YAAY,IAAI,KAAK;CAC5B;CAnYW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAuB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAvB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAuB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,sBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,mBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA;GAAA,YAAA;GAAA,eAAA;GAAA,sBAAA;EAAA;EAAA,MAAA,EAAA,YAAA,EAAA,2BAAA,uBAAA,EAAA;EAAA,aAAA,CAAA;GAAA,cAAA;GAAA,OAAA;GAAA,WA4BA;GAAuB,aAAA;GAAA,UAAA;EAAA,CAAA;EAAA,UAAA;EAAA,UCvF3D;EAyHA,QAAA,CAAA,40CAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MDtEY;IAAgB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAY;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,mBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAuC;IAAgB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,mBAAA,qBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAe,UAAA;IAAA,QAAA,CAAA,iBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAkB,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAqB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAsB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,gBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAA0B,UAAA;IAAA,QAAA,CAAA,qBAAA,4BAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAkB,UAAA;IAAA,QAAA,CAAA,QAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAkB,UAAA;IAAA,QAAA,CAAA,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAuB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAuB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,YAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAsB,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAgB,MAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAQxU,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAuB,YAAA,CAAA;EAVnC,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAkB,SACnB;IAAC;IAAkB;IAAc;IAAkB;IAAmB;IAAkB;IAAiB;IAAoB;IAAuB;IAAwB;IAA4B;IAAoB;IAAoB;IAAyB;IAAyB;IAAwB;GAAgB;GAAC,iBAGnU,wBAAwB;GAAM,MACzC,EACJ,6BAA6B,uBAC9B;GAAA,UAAA;GAAA,QAAA,CAAA,40CAAA;EAAA,CAAA;CA8BiC,CAAA;CAAA,sBAAA,CAAA;CAAA,gBAAA;EAAA,sBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,mBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,UAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA;IAAA,UAAA;IAAA,OAAA;IAAA,UAAA;GAAA,CAAA;EAAA,CAAA;EAAA,YAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,YAAA;EAAA,CAAA;EAAA,eAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,eAAA;EAAA,CAAA;EAAA,sBAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,sBAAA;EAAA,CAAA;EAAA,MAAA,CAAA;GAAA,MAAA,GAAA;GAAA,MAAA,CAAA,GAAA,iBAAA,uBAAuB,GAAA,EAAA,UAAA,KAAA,CAAA;EAAA,CAAA;CAAA;AAAA,CAAA"}
|
|
@@ -21,6 +21,33 @@ function provideSparkAttributeRenderers(renderers) {
|
|
|
21
21
|
useValue: renderers
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
function isRowRendered(attribute, registry) {
|
|
25
|
+
if (!attribute.renderer || !registry?.length) return false;
|
|
26
|
+
return registry.some((r) => r.name === attribute.renderer && !!r.rowComponent);
|
|
27
|
+
}
|
|
28
|
+
function resolveRowRenderer(rowType, registry) {
|
|
29
|
+
if (!rowType || !registry?.length) return null;
|
|
30
|
+
const ordered = [...rowType.attributes ?? []].filter((a) => a.isVisible !== false).sort((a, b) => a.order - b.order);
|
|
31
|
+
for (const attribute of ordered) {
|
|
32
|
+
const registration = attribute.renderer ? registry.find((r) => r.name === attribute.renderer && r.rowComponent) : void 0;
|
|
33
|
+
if (!registration) continue;
|
|
34
|
+
const attributes = ordered.filter((a) => a.renderer === registration.name);
|
|
35
|
+
return {
|
|
36
|
+
component: registration.rowComponent,
|
|
37
|
+
attributes,
|
|
38
|
+
options: attributes[0]?.rendererOptions
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function rowRendererInputs(renderer, row, ownerId) {
|
|
44
|
+
return withDeclaredInputs(renderer.component, {
|
|
45
|
+
row,
|
|
46
|
+
attributes: renderer.attributes,
|
|
47
|
+
options: renderer.options,
|
|
48
|
+
ownerId
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export { SPARK_ATTRIBUTE_RENDERERS, cellValue, isRowRendered, provideSparkAttributeRenderers, rendererValue, resolveRowRenderer, rowRendererInputs, withDeclaredInputs };
|
|
25
52
|
|
|
26
53
|
//# sourceMappingURL=mintplayer-ng-spark-renderers.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mintplayer-ng-spark-renderers.mjs","names":[],"sources":["../../renderers/src/renderer-inputs.ts","../../renderers/src/spark-attribute-renderer-registry.ts"],"sourcesContent":["import { reflectComponentType, Type } from '@angular/core';\nimport { PersistentObjectAttribute, QueryResultItemValue } from '@mintplayer/ng-spark/models';\n\n// Reflection result cached per component type: the input builders are template\n// expressions re-evaluated every CD pass (and query-list virtual-scrolls).\nconst declaredInputs = new Map<Type<any>, Set<string>>();\n\n/**\n * Drops entries the component doesn't declare, so every contract member is\n * genuinely optional — NgComponentOutlet throws on an undeclared input.\n */\nexport function withDeclaredInputs(component: Type<any>, inputs: Record<string, any>): Record<string, any> {\n let declared = declaredInputs.get(component);\n if (!declared) {\n declared = new Set(reflectComponentType(component)?.inputs.map(i => i.templateName) ?? []);\n declaredInputs.set(component, declared);\n }\n return Object.fromEntries(Object.entries(inputs).filter(([k]) => declared!.has(k)));\n}\n\n/**\n * The renderer-facing value of an attribute: the flat value, or for AsDetail\n * attributes (whose flat value the server nulls on purpose) the nested\n * PersistentObject (single) / PersistentObject[] (array).\n *\n * Used by the detail and edit paths, which still work in attributes.\n */\nexport function rendererValue(attr: PersistentObjectAttribute | undefined): any {\n return attr?.value ?? attr?.object ?? attr?.objects;\n}\n\n/**\n * The renderer-facing value of a query-result cell.\n *\n * A cell is a single channel: whatever a renderer needs is already on `value` — for a single-child\n * AsDetail column that is the nested PersistentObject itself, put there by the server so this\n * matches what {@link rendererValue} falls through to on a detail page. There is nothing to fall\n * back to, which is why this is deliberately not the same function: keeping them separate is what\n * stops a renderer silently receiving `undefined` because it was written against the attribute\n * shape's `object` / `objects` fields, which a row does not carry.\n */\nexport function cellValue(value: QueryResultItemValue | undefined): any {\n return value?.value;\n}\n","import { InjectionToken, Provider, Type } from '@angular/core';\n\nexport interface SparkAttributeRendererRegistration {\n /** The renderer name (must match attr.renderer in model JSON) */\n name: string;\n /** Component for the PO detail page. Must implement SparkAttributeDetailRenderer. Omit for a column/edit-only renderer. */\n detailComponent?: Type<any> | null;\n /** Component for query-list column cells. Must implement SparkAttributeColumnRenderer. Omit for a detail/edit-only renderer. */\n columnComponent?: Type<any> | null;\n /** Optional component for create/edit forms. Must implement SparkAttributeEditRenderer. When omitted, the default input is used. */\n editComponent?: Type<any>;\n /**\n * Relabels this column's values in a filter panel (#431).\n *\n * A renderer is a component that paints arbitrary DOM, and a distinct value has no row, so it\n * cannot be instantiated to produce text for a filter list. The label therefore defaults to the\n * server-computed cell text: a status rendered as a coloured pill lists `Active`, a rating\n * rendered as stars lists `3`. This is the escape for when that is too raw — a value stored as\n * `2` whose cell reads \"High priority\".\n *\n * ⚠️ A **pure function**: no DOM, no component instantiation, no row context. There is no row to\n * give it, which is the whole reason the renderer itself cannot be used here.\n */\n filterLabel?: (value: unknown) => string;\n}\n\nexport const SPARK_ATTRIBUTE_RENDERERS = new InjectionToken<SparkAttributeRendererRegistration[]>(\n 'SparkAttributeRenderers',\n { factory: () => [] }\n);\n\n/**\n * Register custom attribute renderers globally.\n *\n * @example\n * provideSparkAttributeRenderers([\n * { name: 'video-player', detailComponent: VideoDetailComponent, columnComponent: VideoColumnComponent },\n * { name: 'color-swatch', detailComponent: ColorDetailComponent, columnComponent: ColorColumnComponent },\n * ])\n */\nexport function provideSparkAttributeRenderers(\n renderers: SparkAttributeRendererRegistration[]\n): Provider {\n return {\n provide: SPARK_ATTRIBUTE_RENDERERS,\n useValue: renderers,\n };\n}\n"],"mappings":";AAKA,MAAM,iCAAiB,IAAI,IAAG;AAM9B,SAAgB,mBAAmB,WAAsB,QAA2B;CAClF,IAAI,WAAW,eAAe,IAAI,SAAS;CAC3C,IAAI,CAAC,UAAU;EACb,WAAW,IAAI,IAAI,qBAAqB,SAAS,CAAC,EAAE,OAAO,KAAI,MAAK,EAAE,YAAY,KAAK,CAAA,CAAE;EACzF,eAAe,IAAI,WAAW,QAAQ;CACxC;CACA,OAAO,OAAO,YAAY,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,CAAC,OAAO,SAAU,IAAI,CAAC,CAAC,CAAC;AACpF;AASA,SAAgB,cAAc,MAA2C;CACvE,OAAO,MAAM,SAAS,MAAM,UAAU,MAAM;AAC9C;AAYA,SAAgB,UAAU,OAAuC;CAC/D,OAAO,OAAO;AAChB;
|
|
1
|
+
{"version":3,"file":"mintplayer-ng-spark-renderers.mjs","names":[],"sources":["../../renderers/src/renderer-inputs.ts","../../renderers/src/spark-attribute-renderer-registry.ts","../../renderers/src/row-renderers.ts"],"sourcesContent":["import { reflectComponentType, Type } from '@angular/core';\nimport { PersistentObjectAttribute, QueryResultItemValue } from '@mintplayer/ng-spark/models';\n\n// Reflection result cached per component type: the input builders are template\n// expressions re-evaluated every CD pass (and query-list virtual-scrolls).\nconst declaredInputs = new Map<Type<any>, Set<string>>();\n\n/**\n * Drops entries the component doesn't declare, so every contract member is\n * genuinely optional — NgComponentOutlet throws on an undeclared input.\n */\nexport function withDeclaredInputs(component: Type<any>, inputs: Record<string, any>): Record<string, any> {\n let declared = declaredInputs.get(component);\n if (!declared) {\n declared = new Set(reflectComponentType(component)?.inputs.map(i => i.templateName) ?? []);\n declaredInputs.set(component, declared);\n }\n return Object.fromEntries(Object.entries(inputs).filter(([k]) => declared!.has(k)));\n}\n\n/**\n * The renderer-facing value of an attribute: the flat value, or for AsDetail\n * attributes (whose flat value the server nulls on purpose) the nested\n * PersistentObject (single) / PersistentObject[] (array).\n *\n * Used by the detail and edit paths, which still work in attributes.\n */\nexport function rendererValue(attr: PersistentObjectAttribute | undefined): any {\n return attr?.value ?? attr?.object ?? attr?.objects;\n}\n\n/**\n * The renderer-facing value of a query-result cell.\n *\n * A cell is a single channel: whatever a renderer needs is already on `value` — for a single-child\n * AsDetail column that is the nested PersistentObject itself, put there by the server so this\n * matches what {@link rendererValue} falls through to on a detail page. There is nothing to fall\n * back to, which is why this is deliberately not the same function: keeping them separate is what\n * stops a renderer silently receiving `undefined` because it was written against the attribute\n * shape's `object` / `objects` fields, which a row does not carry.\n */\nexport function cellValue(value: QueryResultItemValue | undefined): any {\n return value?.value;\n}\n","import { InjectionToken, Provider, Type } from '@angular/core';\n\nexport interface SparkAttributeRendererRegistration {\n /** The renderer name (must match attr.renderer in model JSON) */\n name: string;\n /** Component for the PO detail page. Must implement SparkAttributeDetailRenderer. Omit for a column/edit-only renderer. */\n detailComponent?: Type<any> | null;\n /** Component for query-list column cells. Must implement SparkAttributeColumnRenderer. Omit for a detail/edit-only renderer. */\n columnComponent?: Type<any> | null;\n /** Optional component for create/edit forms. Must implement SparkAttributeEditRenderer. When omitted, the default input is used. */\n editComponent?: Type<any>;\n /**\n * A **row** renderer for AsDetail tables (the PO detail page and the edit form). Attributes of a row\n * type that carry this renderer are not drawn as columns; instead the component is drawn ONCE per\n * row, under the row's first cell, given all of them. Must implement {@link SparkAttributeRowRenderer}.\n *\n * For facts about a row rather than columns of it — contributions' \"by Alice · 3 days ago ·\n * History (4)\" line collapses three read-only attributes into one. `rendererOptions` are taken from\n * the first such attribute (a library writes the same options on each).\n */\n rowComponent?: Type<any>;\n /**\n * Relabels this column's values in a filter panel (#431).\n *\n * A renderer is a component that paints arbitrary DOM, and a distinct value has no row, so it\n * cannot be instantiated to produce text for a filter list. The label therefore defaults to the\n * server-computed cell text: a status rendered as a coloured pill lists `Active`, a rating\n * rendered as stars lists `3`. This is the escape for when that is too raw — a value stored as\n * `2` whose cell reads \"High priority\".\n *\n * ⚠️ A **pure function**: no DOM, no component instantiation, no row context. There is no row to\n * give it, which is the whole reason the renderer itself cannot be used here.\n */\n filterLabel?: (value: unknown) => string;\n}\n\nexport const SPARK_ATTRIBUTE_RENDERERS = new InjectionToken<SparkAttributeRendererRegistration[]>(\n 'SparkAttributeRenderers',\n { factory: () => [] }\n);\n\n/**\n * Register custom attribute renderers globally.\n *\n * @example\n * provideSparkAttributeRenderers([\n * { name: 'video-player', detailComponent: VideoDetailComponent, columnComponent: VideoColumnComponent },\n * { name: 'color-swatch', detailComponent: ColorDetailComponent, columnComponent: ColorColumnComponent },\n * ])\n */\nexport function provideSparkAttributeRenderers(\n renderers: SparkAttributeRendererRegistration[]\n): Provider {\n return {\n provide: SPARK_ATTRIBUTE_RENDERERS,\n useValue: renderers,\n };\n}\n","import { Type } from '@angular/core';\nimport { EntityAttributeDefinition, EntityType } from '@mintplayer/ng-spark/models';\nimport { SparkAttributeRendererRegistration } from './spark-attribute-renderer-registry';\nimport { withDeclaredInputs } from './renderer-inputs';\n\n/** The row renderer of an AsDetail row type, resolved once per type (see `rowComponent`). */\nexport interface SparkResolvedRowRenderer {\n component: Type<any>;\n /** The row-type attributes carrying the renderer, in model order. */\n attributes: EntityAttributeDefinition[];\n /** `rendererOptions` of the first of them. */\n options: Record<string, any> | undefined;\n}\n\n/** Whether `attribute` is drawn by a row renderer (and therefore not as a column). */\nexport function isRowRendered(attribute: EntityAttributeDefinition, registry: readonly SparkAttributeRendererRegistration[] | null | undefined): boolean {\n if (!attribute.renderer || !registry?.length) return false;\n return registry.some(r => r.name === attribute.renderer && !!r.rowComponent);\n}\n\n/**\n * The first row renderer the row type's visible attributes name, or null. One per row: a row type\n * whose attributes name two different row renderers gets the first in model order.\n */\nexport function resolveRowRenderer(\n rowType: EntityType | null | undefined,\n registry: readonly SparkAttributeRendererRegistration[] | null | undefined,\n): SparkResolvedRowRenderer | null {\n if (!rowType || !registry?.length) return null;\n const ordered = [...(rowType.attributes ?? [])].filter(a => a.isVisible !== false).sort((a, b) => a.order - b.order);\n for (const attribute of ordered) {\n const registration = attribute.renderer ? registry.find(r => r.name === attribute.renderer && r.rowComponent) : undefined;\n if (!registration) continue;\n const attributes = ordered.filter(a => a.renderer === registration.name);\n return { component: registration.rowComponent!, attributes, options: attributes[0]?.rendererOptions };\n }\n return null;\n}\n\n/** The input bag for a row renderer, filtered to what the component declares. */\nexport function rowRendererInputs(renderer: SparkResolvedRowRenderer, row: Record<string, any>, ownerId: string | undefined): Record<string, any> {\n return withDeclaredInputs(renderer.component, {\n row,\n attributes: renderer.attributes,\n options: renderer.options,\n ownerId,\n });\n}\n"],"mappings":";AAKA,MAAM,iCAAiB,IAAI,IAAG;AAM9B,SAAgB,mBAAmB,WAAsB,QAA2B;CAClF,IAAI,WAAW,eAAe,IAAI,SAAS;CAC3C,IAAI,CAAC,UAAU;EACb,WAAW,IAAI,IAAI,qBAAqB,SAAS,CAAC,EAAE,OAAO,KAAI,MAAK,EAAE,YAAY,KAAK,CAAA,CAAE;EACzF,eAAe,IAAI,WAAW,QAAQ;CACxC;CACA,OAAO,OAAO,YAAY,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,CAAC,OAAO,SAAU,IAAI,CAAC,CAAC,CAAC;AACpF;AASA,SAAgB,cAAc,MAA2C;CACvE,OAAO,MAAM,SAAS,MAAM,UAAU,MAAM;AAC9C;AAYA,SAAgB,UAAU,OAAuC;CAC/D,OAAO,OAAO;AAChB;ACPA,MAAa,4BAA4B,IAAI,eAC3C,2BACA,EAAE,eAAe,CAAA,EAAE,CAAE;AAYvB,SAAgB,+BACd,WAA+C;CAE/C,OAAO;EACL,SAAS;EACT,UAAU;CACX;AACH;AC1CA,SAAgB,cAAc,WAAsC,UAA0E;CAC5I,IAAI,CAAC,UAAU,YAAY,CAAC,UAAU,QAAQ,OAAO;CACrD,OAAO,SAAS,MAAK,MAAK,EAAE,SAAS,UAAU,YAAY,CAAC,CAAC,EAAE,YAAY;AAC7E;AAMA,SAAgB,mBACd,SACA,UAA0E;CAE1E,IAAI,CAAC,WAAW,CAAC,UAAU,QAAQ,OAAO;CAC1C,MAAM,UAAU,CAAC,GAAI,QAAQ,cAAc,CAAA,CAAG,CAAC,CAAC,QAAO,MAAK,EAAE,cAAc,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;CACnH,KAAK,MAAM,aAAa,SAAS;EAC/B,MAAM,eAAe,UAAU,WAAW,SAAS,MAAK,MAAK,EAAE,SAAS,UAAU,YAAY,EAAE,YAAY,IAAI;EAChH,IAAI,CAAC,cAAc;EACnB,MAAM,aAAa,QAAQ,QAAO,MAAK,EAAE,aAAa,aAAa,IAAI;EACvE,OAAO;GAAE,WAAW,aAAa;GAAe;GAAY,SAAS,WAAW,EAAE,EAAE;EAAe;CACrG;CACA,OAAO;AACT;AAGA,SAAgB,kBAAkB,UAAoC,KAA0B,SAA2B;CACzH,OAAO,mBAAmB,SAAS,WAAW;EAC5C;EACA,YAAY,SAAS;EACrB,SAAS,SAAS;EAClB;CACD,CAAA;AACH"}
|
|
@@ -3,10 +3,11 @@ import { ChangeDetectionStrategy, Component, Directive, TemplateRef, computed, c
|
|
|
3
3
|
import { KeyValuePipe, NgTemplateOutlet } from "@angular/common";
|
|
4
4
|
import { BsShellComponent, BsShellSidebarDirective } from "@mintplayer/ng-bootstrap/shell";
|
|
5
5
|
import { BsNavbarTogglerComponent } from "@mintplayer/ng-bootstrap/navbar-toggler";
|
|
6
|
+
import { BS_THEME_DEFAULT_MODES, BsThemeService, BsThemeToggleComponent } from "@mintplayer/ng-bootstrap/theming";
|
|
7
|
+
import { SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
|
|
6
8
|
import { RouterLink, RouterLinkActive } from "@angular/router";
|
|
7
9
|
import { BsAccordionComponent, BsAccordionTabComponent, BsAccordionTabHeaderDirective } from "@mintplayer/ng-bootstrap/accordion";
|
|
8
10
|
import { SPARK_AUTH_STATE } from "@mintplayer/ng-spark";
|
|
9
|
-
import { SparkLanguageService, SparkService } from "@mintplayer/ng-spark/services";
|
|
10
11
|
import { IconNamePipe, ResolveTranslationPipe, RouterLinkPipe } from "@mintplayer/ng-spark/pipes";
|
|
11
12
|
import { SparkIconComponent } from "@mintplayer/ng-spark/icon";
|
|
12
13
|
import * as i1 from "@angular/forms";
|
|
@@ -64,7 +65,7 @@ var SparkProgramUnitsComponent = class SparkProgramUnitsComponent {
|
|
|
64
65
|
},
|
|
65
66
|
ngImport: i0,
|
|
66
67
|
template: "<bs-accordion class=\"d-block\">\n @for (group of groups(); track group.id) {\n <bs-accordion-tab>\n <ng-container *bsAccordionTabHeader>\n <spark-icon [name]=\"group.icon | iconName:'bi-folder'\" />\n <span class=\"ms-2\">{{ group.name | resolveTranslation }}</span>\n </ng-container>\n @for (unit of group.programUnits; track unit.id) {\n @if (unit | routerLink; as link) {\n <a [routerLink]=\"link\"\n routerLinkActive=\"active\"\n class=\"d-block px-3 py-2 text-decoration-none nav-link\">\n <spark-icon [name]=\"unit.icon | iconName:'bi-file'\" />\n <span class=\"ms-2\">{{ unit.name | resolveTranslation }}</span>\n </a>\n } @else {\n <!-- A url unit is an external address: a plain anchor, never a router link. -->\n <a [href]=\"unit.url\" target=\"_blank\" rel=\"noopener\"\n class=\"d-block px-3 py-2 text-decoration-none nav-link\">\n <spark-icon [name]=\"unit.icon | iconName:'bi-box-arrow-up-right'\" />\n <span class=\"ms-2\">{{ unit.name | resolveTranslation }}</span>\n </a>\n }\n }\n </bs-accordion-tab>\n }\n <!-- Host-contributed tabs are rendered HERE, by this template, so that the accordion's content\n query registers them and they join the generated groups' single-open group. -->\n @for (tab of extraTabs(); track tab.content) {\n <bs-accordion-tab>\n <ng-container *bsAccordionTabHeader>\n @if (tab.headerTemplate; as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-icon [name]=\"tab.icon | iconName:'bi-folder'\" />\n <span class=\"ms-2\">{{ tab.header }}</span>\n }\n </ng-container>\n <ng-container *ngTemplateOutlet=\"tab.content\" />\n </bs-accordion-tab>\n }\n</bs-accordion>\n",
|
|
67
|
-
styles: [":host{display:block;--bs-primary-bg-subtle: rgba(
|
|
68
|
+
styles: [":host{display:block;--bs-primary-bg-subtle: rgba(var(--bs-emphasis-color-rgb), .1);--bs-border-color: transparent;--bs-body-bg: var(--spark-shell-sidebar-bg, var(--bs-tertiary-bg))}:host ::ng-deep mp-accordion::part(content){padding:0}.nav-link{color:inherit}.nav-link:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.1)}.nav-link.active{background-color:rgba(var(--bs-emphasis-color-rgb),.2)}\n"],
|
|
68
69
|
dependencies: [
|
|
69
70
|
{
|
|
70
71
|
kind: "directive",
|
|
@@ -174,7 +175,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
174
175
|
RouterLinkPipe
|
|
175
176
|
],
|
|
176
177
|
template: "<bs-accordion class=\"d-block\">\n @for (group of groups(); track group.id) {\n <bs-accordion-tab>\n <ng-container *bsAccordionTabHeader>\n <spark-icon [name]=\"group.icon | iconName:'bi-folder'\" />\n <span class=\"ms-2\">{{ group.name | resolveTranslation }}</span>\n </ng-container>\n @for (unit of group.programUnits; track unit.id) {\n @if (unit | routerLink; as link) {\n <a [routerLink]=\"link\"\n routerLinkActive=\"active\"\n class=\"d-block px-3 py-2 text-decoration-none nav-link\">\n <spark-icon [name]=\"unit.icon | iconName:'bi-file'\" />\n <span class=\"ms-2\">{{ unit.name | resolveTranslation }}</span>\n </a>\n } @else {\n <!-- A url unit is an external address: a plain anchor, never a router link. -->\n <a [href]=\"unit.url\" target=\"_blank\" rel=\"noopener\"\n class=\"d-block px-3 py-2 text-decoration-none nav-link\">\n <spark-icon [name]=\"unit.icon | iconName:'bi-box-arrow-up-right'\" />\n <span class=\"ms-2\">{{ unit.name | resolveTranslation }}</span>\n </a>\n }\n }\n </bs-accordion-tab>\n }\n <!-- Host-contributed tabs are rendered HERE, by this template, so that the accordion's content\n query registers them and they join the generated groups' single-open group. -->\n @for (tab of extraTabs(); track tab.content) {\n <bs-accordion-tab>\n <ng-container *bsAccordionTabHeader>\n @if (tab.headerTemplate; as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-icon [name]=\"tab.icon | iconName:'bi-folder'\" />\n <span class=\"ms-2\">{{ tab.header }}</span>\n }\n </ng-container>\n <ng-container *ngTemplateOutlet=\"tab.content\" />\n </bs-accordion-tab>\n }\n</bs-accordion>\n",
|
|
177
|
-
styles: [":host{display:block;--bs-primary-bg-subtle: rgba(
|
|
178
|
+
styles: [":host{display:block;--bs-primary-bg-subtle: rgba(var(--bs-emphasis-color-rgb), .1);--bs-border-color: transparent;--bs-body-bg: var(--spark-shell-sidebar-bg, var(--bs-tertiary-bg))}:host ::ng-deep mp-accordion::part(content){padding:0}.nav-link{color:inherit}.nav-link:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.1)}.nav-link.active{background-color:rgba(var(--bs-emphasis-color-rgb),.2)}\n"]
|
|
178
179
|
}]
|
|
179
180
|
}],
|
|
180
181
|
ctorParameters: () => [],
|
|
@@ -621,7 +622,17 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
621
622
|
var SparkShellComponent = class SparkShellComponent {
|
|
622
623
|
title = input("", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
623
624
|
breakpoint = input("md", ...ngDevMode ? [{ debugName: "breakpoint" }] : /* istanbul ignore next */ []);
|
|
624
|
-
|
|
625
|
+
themeToggle = input(true, ...ngDevMode ? [{ debugName: "themeToggle" }] : /* istanbul ignore next */ []);
|
|
626
|
+
theme = inject(BsThemeService);
|
|
627
|
+
lang = inject(SparkLanguageService);
|
|
628
|
+
themeModes = computed(() => BS_THEME_DEFAULT_MODES.map((m) => {
|
|
629
|
+
const name = m.mode.charAt(0).toUpperCase() + m.mode.slice(1);
|
|
630
|
+
return {
|
|
631
|
+
...m,
|
|
632
|
+
label: this.lang.t(`theme.switchTo${name}`),
|
|
633
|
+
announcement: this.lang.t(`theme.${m.mode}`)
|
|
634
|
+
};
|
|
635
|
+
}), ...ngDevMode ? [{ debugName: "themeModes" }] : /* istanbul ignore next */ []);
|
|
625
636
|
reloadToken = input(null, ...ngDevMode ? [{ debugName: "reloadToken" }] : /* istanbul ignore next */ []);
|
|
626
637
|
sidebarTabs = input([], ...ngDevMode ? [{ debugName: "sidebarTabs" }] : /* istanbul ignore next */ []);
|
|
627
638
|
topbarStartTemplate = input(null, ...ngDevMode ? [{ debugName: "topbarStartTemplate" }] : /* istanbul ignore next */ []);
|
|
@@ -689,9 +700,9 @@ var SparkShellComponent = class SparkShellComponent {
|
|
|
689
700
|
isRequired: false,
|
|
690
701
|
transformFunction: null
|
|
691
702
|
},
|
|
692
|
-
|
|
693
|
-
classPropertyName: "
|
|
694
|
-
publicName: "
|
|
703
|
+
themeToggle: {
|
|
704
|
+
classPropertyName: "themeToggle",
|
|
705
|
+
publicName: "themeToggle",
|
|
695
706
|
isSignal: true,
|
|
696
707
|
isRequired: false,
|
|
697
708
|
transformFunction: null
|
|
@@ -817,8 +828,8 @@ var SparkShellComponent = class SparkShellComponent {
|
|
|
817
828
|
}
|
|
818
829
|
],
|
|
819
830
|
ngImport: i0,
|
|
820
|
-
template: "<bs-shell [state]=\"shellState()\" [breakpoint]=\"breakpoint()\" [dismissOnNavigate]=\"true\"\n (statechange)=\"onShellToggle($event)\">\n <div slot=\"topbar\" class=\"spark-topbar w-100 d-flex align-items-center\">\n @if (topbarStartTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <bs-navbar-toggler [state]=\"isSidebarVisible()\" (stateChange)=\"toggleSidebar($event)\" />\n }\n <div class=\"ms-auto me-3 d-flex align-items-center gap-2\">\n <!-- Ahead of the trailing chrome, and OUTSIDE the @if below: these actions are additive, so\n they render whether the host replaced the trailing edge or left the default selector. -->\n @if (topbarActionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n @if (topbarEndTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-language-selector />\n }\n </div>\n </div>\n\n <nav bsShellSidebar class=\"spark-sidebar p-2\"
|
|
821
|
-
styles: [":host{display:flex;flex-direction:column;height:100vh}bs-shell{flex:1 1 auto;min-height:0}.spark-topbar{background-color:var(--spark-shell-topbar-bg, var(--bs-
|
|
831
|
+
template: "<bs-shell [state]=\"shellState()\" [breakpoint]=\"breakpoint()\" [dismissOnNavigate]=\"true\"\n (statechange)=\"onShellToggle($event)\">\n <div slot=\"topbar\" class=\"spark-topbar w-100 d-flex align-items-center\">\n @if (topbarStartTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <bs-navbar-toggler [state]=\"isSidebarVisible()\" (stateChange)=\"toggleSidebar($event)\" />\n }\n <div class=\"ms-auto me-3 d-flex align-items-center gap-2\">\n <!-- Ahead of the trailing chrome, and OUTSIDE the @if below: these actions are additive, so\n they render whether the host replaced the trailing edge or left the default selector. -->\n @if (topbarActionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <!-- Also outside the trailing @if: replacing the end slot (e.g. with spark-auth-bar) keeps it. -->\n @if (themeToggle()) {\n <bs-theme-toggle [modes]=\"themeModes()\" />\n }\n @if (topbarEndTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-language-selector />\n }\n </div>\n </div>\n\n <nav bsShellSidebar class=\"spark-sidebar p-2\">\n @if (sidebarHeaderTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <h5 class=\"p-3 border-bottom\">{{ title() }}</h5>\n }\n @if (sidebarTopTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <spark-program-units [reloadToken]=\"reloadToken()\" [extraTabs]=\"tabs()\" />\n @if (sidebarFooterTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n </nav>\n\n <!-- The padding lives in the stylesheet as `--spark-main-padding`, not as a `p-4` utility, so the\n sticky action bar can read it and bleed back out to the scrollport edges. See _actionbar.scss. -->\n <main>\n @if (mainHeaderTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <ng-content />\n </main>\n</bs-shell>\n",
|
|
832
|
+
styles: ["::ng-deep :root,::ng-deep [data-bs-theme=light]{--spark-shell-topbar-bg: var(--bs-tertiary-bg);--spark-shell-topbar-color: var(--bs-body-color);--spark-shell-sidebar-bg: var(--bs-tertiary-bg);--spark-shell-sidebar-color: var(--bs-body-color);--spark-shell-main-bg: var(--bs-body-bg);--spark-shell-main-color: var(--bs-body-color)}::ng-deep [data-bs-theme=dark]{--spark-shell-topbar-bg: var(--bs-tertiary-bg);--spark-shell-topbar-color: var(--bs-body-color);--spark-shell-sidebar-bg: var(--bs-tertiary-bg);--spark-shell-sidebar-color: var(--bs-body-color);--spark-shell-main-bg: var(--bs-body-bg);--spark-shell-main-color: var(--bs-body-color)}:host{display:flex;flex-direction:column;height:100vh;--spark-shell-border-color: var(--bs-border-color)}bs-shell{flex:1 1 auto;min-height:0}.spark-topbar{background-color:var(--spark-shell-topbar-bg, var(--bs-tertiary-bg));color:var(--spark-shell-topbar-color, var(--bs-body-color));border-bottom:var(--bs-border-width) solid var(--spark-shell-border-color)}::ng-deep spark-shell mp-shell::part(hamburger){display:none}bs-navbar-toggler{--bs-secondary-color: var(--spark-shell-topbar-color, var(--bs-body-color))}main{--spark-main-padding: 1.5rem;--spark-content-inset-block: var(--spark-main-padding);--spark-content-inset-inline: var(--spark-main-padding);height:100%;padding:var(--spark-main-padding);background-color:var(--spark-shell-main-bg, var(--bs-body-bg));color:var(--spark-shell-main-color, var(--bs-body-color));display:flex;flex-direction:column;overflow:auto}nav.spark-sidebar{min-height:100%;background:var(--spark-shell-sidebar-bg, var(--bs-tertiary-bg));color:var(--spark-shell-sidebar-color, var(--bs-body-color));border-inline-end:var(--bs-border-width) solid var(--spark-shell-border-color)}:host ::ng-deep nav.spark-sidebar{--bs-primary-bg-subtle: rgba(var(--bs-emphasis-color-rgb), .1);--bs-border-color: transparent}:host ::ng-deep nav.spark-sidebar mp-accordion::part(content){padding:0}:host ::ng-deep nav.spark-sidebar .nav-link{color:inherit}:host ::ng-deep nav.spark-sidebar .nav-link:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.1)}:host ::ng-deep nav.spark-sidebar .nav-link.active{background-color:rgba(var(--bs-emphasis-color-rgb),.2)}\n"],
|
|
822
833
|
dependencies: [
|
|
823
834
|
{
|
|
824
835
|
kind: "directive",
|
|
@@ -858,6 +869,12 @@ var SparkShellComponent = class SparkShellComponent {
|
|
|
858
869
|
],
|
|
859
870
|
outputs: ["stateChange"]
|
|
860
871
|
},
|
|
872
|
+
{
|
|
873
|
+
kind: "component",
|
|
874
|
+
type: BsThemeToggleComponent,
|
|
875
|
+
selector: "bs-theme-toggle",
|
|
876
|
+
inputs: ["modes"]
|
|
877
|
+
},
|
|
861
878
|
{
|
|
862
879
|
kind: "component",
|
|
863
880
|
type: SparkProgramUnitsComponent,
|
|
@@ -889,11 +906,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
889
906
|
BsShellComponent,
|
|
890
907
|
BsShellSidebarDirective,
|
|
891
908
|
BsNavbarTogglerComponent,
|
|
909
|
+
BsThemeToggleComponent,
|
|
892
910
|
SparkProgramUnitsComponent,
|
|
893
911
|
SparkLanguageSelectorComponent
|
|
894
912
|
],
|
|
895
|
-
template: "<bs-shell [state]=\"shellState()\" [breakpoint]=\"breakpoint()\" [dismissOnNavigate]=\"true\"\n (statechange)=\"onShellToggle($event)\">\n <div slot=\"topbar\" class=\"spark-topbar w-100 d-flex align-items-center\">\n @if (topbarStartTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <bs-navbar-toggler [state]=\"isSidebarVisible()\" (stateChange)=\"toggleSidebar($event)\" />\n }\n <div class=\"ms-auto me-3 d-flex align-items-center gap-2\">\n <!-- Ahead of the trailing chrome, and OUTSIDE the @if below: these actions are additive, so\n they render whether the host replaced the trailing edge or left the default selector. -->\n @if (topbarActionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n @if (topbarEndTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-language-selector />\n }\n </div>\n </div>\n\n <nav bsShellSidebar class=\"spark-sidebar p-2\"
|
|
896
|
-
styles: [":host{display:flex;flex-direction:column;height:100vh}bs-shell{flex:1 1 auto;min-height:0}.spark-topbar{background-color:var(--spark-shell-topbar-bg, var(--bs-
|
|
913
|
+
template: "<bs-shell [state]=\"shellState()\" [breakpoint]=\"breakpoint()\" [dismissOnNavigate]=\"true\"\n (statechange)=\"onShellToggle($event)\">\n <div slot=\"topbar\" class=\"spark-topbar w-100 d-flex align-items-center\">\n @if (topbarStartTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <bs-navbar-toggler [state]=\"isSidebarVisible()\" (stateChange)=\"toggleSidebar($event)\" />\n }\n <div class=\"ms-auto me-3 d-flex align-items-center gap-2\">\n <!-- Ahead of the trailing chrome, and OUTSIDE the @if below: these actions are additive, so\n they render whether the host replaced the trailing edge or left the default selector. -->\n @if (topbarActionsTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <!-- Also outside the trailing @if: replacing the end slot (e.g. with spark-auth-bar) keeps it. -->\n @if (themeToggle()) {\n <bs-theme-toggle [modes]=\"themeModes()\" />\n }\n @if (topbarEndTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <spark-language-selector />\n }\n </div>\n </div>\n\n <nav bsShellSidebar class=\"spark-sidebar p-2\">\n @if (sidebarHeaderTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else {\n <h5 class=\"p-3 border-bottom\">{{ title() }}</h5>\n }\n @if (sidebarTopTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <spark-program-units [reloadToken]=\"reloadToken()\" [extraTabs]=\"tabs()\" />\n @if (sidebarFooterTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n </nav>\n\n <!-- The padding lives in the stylesheet as `--spark-main-padding`, not as a `p-4` utility, so the\n sticky action bar can read it and bleed back out to the scrollport edges. See _actionbar.scss. -->\n <main>\n @if (mainHeaderTpl(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n }\n <ng-content />\n </main>\n</bs-shell>\n",
|
|
914
|
+
styles: ["::ng-deep :root,::ng-deep [data-bs-theme=light]{--spark-shell-topbar-bg: var(--bs-tertiary-bg);--spark-shell-topbar-color: var(--bs-body-color);--spark-shell-sidebar-bg: var(--bs-tertiary-bg);--spark-shell-sidebar-color: var(--bs-body-color);--spark-shell-main-bg: var(--bs-body-bg);--spark-shell-main-color: var(--bs-body-color)}::ng-deep [data-bs-theme=dark]{--spark-shell-topbar-bg: var(--bs-tertiary-bg);--spark-shell-topbar-color: var(--bs-body-color);--spark-shell-sidebar-bg: var(--bs-tertiary-bg);--spark-shell-sidebar-color: var(--bs-body-color);--spark-shell-main-bg: var(--bs-body-bg);--spark-shell-main-color: var(--bs-body-color)}:host{display:flex;flex-direction:column;height:100vh;--spark-shell-border-color: var(--bs-border-color)}bs-shell{flex:1 1 auto;min-height:0}.spark-topbar{background-color:var(--spark-shell-topbar-bg, var(--bs-tertiary-bg));color:var(--spark-shell-topbar-color, var(--bs-body-color));border-bottom:var(--bs-border-width) solid var(--spark-shell-border-color)}::ng-deep spark-shell mp-shell::part(hamburger){display:none}bs-navbar-toggler{--bs-secondary-color: var(--spark-shell-topbar-color, var(--bs-body-color))}main{--spark-main-padding: 1.5rem;--spark-content-inset-block: var(--spark-main-padding);--spark-content-inset-inline: var(--spark-main-padding);height:100%;padding:var(--spark-main-padding);background-color:var(--spark-shell-main-bg, var(--bs-body-bg));color:var(--spark-shell-main-color, var(--bs-body-color));display:flex;flex-direction:column;overflow:auto}nav.spark-sidebar{min-height:100%;background:var(--spark-shell-sidebar-bg, var(--bs-tertiary-bg));color:var(--spark-shell-sidebar-color, var(--bs-body-color));border-inline-end:var(--bs-border-width) solid var(--spark-shell-border-color)}:host ::ng-deep nav.spark-sidebar{--bs-primary-bg-subtle: rgba(var(--bs-emphasis-color-rgb), .1);--bs-border-color: transparent}:host ::ng-deep nav.spark-sidebar mp-accordion::part(content){padding:0}:host ::ng-deep nav.spark-sidebar .nav-link{color:inherit}:host ::ng-deep nav.spark-sidebar .nav-link:hover{background-color:rgba(var(--bs-emphasis-color-rgb),.1)}:host ::ng-deep nav.spark-sidebar .nav-link.active{background-color:rgba(var(--bs-emphasis-color-rgb),.2)}\n"]
|
|
897
915
|
}]
|
|
898
916
|
}],
|
|
899
917
|
propDecorators: {
|
|
@@ -913,11 +931,11 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
913
931
|
required: false
|
|
914
932
|
}]
|
|
915
933
|
}],
|
|
916
|
-
|
|
934
|
+
themeToggle: [{
|
|
917
935
|
type: i0.Input,
|
|
918
936
|
args: [{
|
|
919
937
|
isSignal: true,
|
|
920
|
-
alias: "
|
|
938
|
+
alias: "themeToggle",
|
|
921
939
|
required: false
|
|
922
940
|
}]
|
|
923
941
|
}],
|