@firestitch/report 18.3.1 → 18.4.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.
@@ -2,10 +2,10 @@ import * as i0 from '@angular/core';
2
2
  import { inject, Injectable, Component, ChangeDetectionStrategy, Input, signal, DestroyRef, ViewChild, input, output, computed, viewChild, ElementRef, effect, untracked, EventEmitter, ChangeDetectorRef, NgZone, Output, HostBinding, ViewChildren, CUSTOM_ELEMENTS_SCHEMA, model } from '@angular/core';
3
3
  import * as i1$1 from '@angular/forms';
4
4
  import { FormsModule, ControlContainer, NgForm } from '@angular/forms';
5
- import * as i3$4 from '@angular/material/button';
5
+ import * as i3$3 from '@angular/material/button';
6
6
  import { MatIconButton, MatButton, MatButtonModule } from '@angular/material/button';
7
7
  import { MAT_DIALOG_DATA, MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose, MatDialog } from '@angular/material/dialog';
8
- import * as i4$1 from '@angular/material/icon';
8
+ import * as i5$2 from '@angular/material/icon';
9
9
  import { MatIcon, MatIconModule } from '@angular/material/icon';
10
10
  import * as i2$5 from '@angular/material/tooltip';
11
11
  import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
@@ -57,12 +57,12 @@ import { FsDatePickerModule } from '@firestitch/datepicker';
57
57
  import { MatOption } from '@angular/material/core';
58
58
  import { guid } from '@firestitch/common';
59
59
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
60
+ import * as i1$4 from '@firestitch/chip';
61
+ import { FsChipModule } from '@firestitch/chip';
60
62
  import * as i4 from '@firestitch/skeleton';
61
63
  import { FsSkeletonModule } from '@firestitch/skeleton';
62
- import * as i3$3 from '@firestitch/tree';
64
+ import * as i4$1 from '@firestitch/tree';
63
65
  import { FsTreeComponent, TreeDragMode, TreeActionType, FsTreeModule } from '@firestitch/tree';
64
- import * as i1$4 from '@firestitch/chip';
65
- import { FsChipModule } from '@firestitch/chip';
66
66
  import * as i2$4 from '@firestitch/date';
67
67
  import { FsDateModule } from '@firestitch/date';
68
68
  import { FsMarkdownRendererComponent, FsMarkdownEditorComponent } from '@firestitch/markdown-editor';
@@ -123,14 +123,23 @@ class ReportData {
123
123
  ...config,
124
124
  });
125
125
  }
126
- // A real delete — the backend refuses while the folder still holds a
127
- // sub-folder or an active report, so there is no undelete counterpart.
126
+ // Soft-delete, whatever the folder holds: its contents leave the listing with
127
+ // it, and undeleteFolder() brings the lot back.
128
128
  deleteFolder(reportFolderId, config = {}) {
129
129
  return this._api.delete(this._path(`folders/${reportFolderId}`), {}, {
130
130
  key: 'folder',
131
131
  ...config,
132
132
  });
133
133
  }
134
+ // Restore a soft-deleted folder through its own action endpoint — the folder
135
+ // PUT does not accept `state`. Any deleted folder above it is restored too, so
136
+ // it lands back where it was.
137
+ undeleteFolder(reportFolderId, config = {}) {
138
+ return this._api.post(this._path(`folders/${reportFolderId}/undelete`), {}, {
139
+ key: 'folder',
140
+ ...config,
141
+ });
142
+ }
134
143
  rename(reportId, name, config = {}) {
135
144
  return this._api.put(this._path(`${reportId}`), { name }, {
136
145
  key: 'report',
@@ -216,7 +225,8 @@ class ReportData {
216
225
  });
217
226
  }
218
227
  // Restore a soft-deleted report through its own action endpoint — the report
219
- // PUT does not accept `state`.
228
+ // PUT does not accept `state`. Its folder is restored too when it is deleted,
229
+ // or inside one that is, so the report lands back where it was.
220
230
  undelete(reportId, config = {}) {
221
231
  return this._api.post(this._path(`${reportId}/undelete`), {}, {
222
232
  key: 'report',
@@ -493,19 +503,15 @@ var ReportNodeType;
493
503
  ReportNodeType["Folder"] = "folder";
494
504
  ReportNodeType["Report"] = "report";
495
505
  })(ReportNodeType || (ReportNodeType = {}));
496
- // Reports are soft-deletable: a deleted report leaves the listing but keeps its
497
- // pages and components, and the listing's "Show Deleted" mode restores it.
498
- // Folders have no equivalent — they are only deletable once empty, so they hard
499
- // delete and there is nothing to restore.
506
+ // Reports and folders are both soft-deletable. A deleted report keeps its pages
507
+ // and components; a deleted folder keeps everything filed under it, which leaves
508
+ // the listing with it and comes back with it. "Show Deleted" shows the whole
509
+ // tree, live and deleted together, so any of it can be restored in place.
500
510
  var ReportState;
501
511
  (function (ReportState) {
502
512
  ReportState["Active"] = "active";
503
513
  ReportState["Deleted"] = "deleted";
504
514
  })(ReportState || (ReportState = {}));
505
- const ReportStates = [
506
- { name: 'Active', value: ReportState.Active },
507
- { name: 'Deleted', value: ReportState.Deleted },
508
- ];
509
515
 
510
516
  // A date-range boundary as the picked LOCAL calendar day (no time, no tz).
511
517
  // Date-range filters are calendar-day semantics, so the boundary must carry
@@ -990,8 +996,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
990
996
  // unrelated groups. Explicitly opting out — colorBy: {column: ''} or a
991
997
  // config.legend of 'hidden' — still wins, so a caller that wants one flat color
992
998
  // can say so.
999
+ //
1000
+ // "Bar chart" is the type the lone measure is actually DRAWN as: its own type
1001
+ // when it names one, which can turn a line chart's single series into bars or
1002
+ // a bar chart's into a line.
993
1003
  function isColorBy(config) {
994
- if (config.splitBy?.column || (config.chartType ?? 'bar') !== 'bar') {
1004
+ if (config.splitBy?.column || (config.measures?.[0]?.type ?? config.chartType ?? 'bar') !== 'bar') {
995
1005
  return false;
996
1006
  }
997
1007
  if (config.colorBy) {
@@ -1035,6 +1045,7 @@ function extractChartSeries(component, data) {
1035
1045
  series.push({
1036
1046
  name: String(groupValue ?? '—'),
1037
1047
  values: rawCategories.map((category) => numeric(byCategory.get(category))),
1048
+ type: measure.type,
1038
1049
  });
1039
1050
  }
1040
1051
  }
@@ -1047,11 +1058,19 @@ function extractChartSeries(component, data) {
1047
1058
  series.push({
1048
1059
  name: measure.label ?? measure.column,
1049
1060
  values: rawCategories.map((category) => numeric(byCategory.get(category))),
1061
+ type: measure.type,
1050
1062
  });
1051
1063
  }
1052
1064
  }
1053
1065
  return { categories, series };
1054
1066
  }
1067
+ // The type each extracted series is drawn as: its measure's own type when it
1068
+ // names one, the chart's chartType otherwise. More than one distinct type is a
1069
+ // combo chart. Shared with the PowerPoint export so both media draw each series
1070
+ // the same way.
1071
+ function seriesTypes(extracted, chartType) {
1072
+ return extracted.series.map((series) => series.type ?? chartType);
1073
+ }
1055
1074
  // The one pure mapping from our standard camelCase chart config + the data
1056
1075
  // endpoint's rows to an ECharts option. Everything visual that isn't in the
1057
1076
  // registered 'report' theme is decided here, so report standards have exactly
@@ -1069,11 +1088,17 @@ function buildAxisOption(component, data, chartType) {
1069
1088
  const extracted = extractChartSeries(component, data);
1070
1089
  const horizontal = config.orientation === 'horizontal';
1071
1090
  const colorBy = isColorBy(config);
1091
+ const types = seriesTypes(extracted, chartType);
1092
+ const combo = new Set(types).size > 1;
1072
1093
  // colorBy emits one series per category; overlapping them (barGap -100%) makes
1073
1094
  // each render at the full category-band width — one full-width, distinctly
1074
1095
  // colored bar per category — instead of side-by-side slivers. The layout props
1075
1096
  // are read from the first series of the coordinate system.
1076
- const series = extracted.series.map((extractedSeries, index) => buildSeries(extractedSeries.name, extractedSeries.values, chartType, config.stacked, colorBy && index === 0));
1097
+ //
1098
+ // Stacking sums every series that shares a stack name, whatever its type. In
1099
+ // a combo each type stacks only with its own kind, so a line of a rate never
1100
+ // sits on top of the bars' total.
1101
+ const series = extracted.series.map((extractedSeries, index) => buildSeries(extractedSeries.name, extractedSeries.values, types[index], config.stacked ? (combo ? `total-${types[index]}` : 'total') : undefined, colorBy && index === 0, combo));
1077
1102
  const granularity = bucketGranularity(data);
1078
1103
  const categoryAxis = {
1079
1104
  type: 'category',
@@ -1143,13 +1168,13 @@ function axisGrid(position) {
1143
1168
  return { ...base, top: 32 };
1144
1169
  }
1145
1170
  }
1146
- function buildSeries(name, values, chartType, stacked, overlap) {
1147
- if (chartType === 'bar') {
1171
+ function buildSeries(name, values, type, stack, overlap, combo) {
1172
+ if (type === 'bar') {
1148
1173
  return {
1149
1174
  name,
1150
1175
  type: 'bar',
1151
1176
  data: values,
1152
- stack: stacked ? 'total' : undefined,
1177
+ stack,
1153
1178
  barMaxWidth: 48,
1154
1179
  // colorBy: overlap the per-category series so each bar renders full-width.
1155
1180
  ...(overlap ? { barGap: '-100%', barCategoryGap: '20%' } : {}),
@@ -1159,10 +1184,13 @@ function buildSeries(name, values, chartType, stacked, overlap) {
1159
1184
  name,
1160
1185
  type: 'line',
1161
1186
  data: values,
1162
- stack: stacked ? 'total' : undefined,
1187
+ stack,
1163
1188
  smooth: true,
1164
1189
  symbolSize: 6,
1165
- areaStyle: chartType === 'area' ? { opacity: 0.25 } : undefined,
1190
+ areaStyle: type === 'area' ? { opacity: 0.25 } : undefined,
1191
+ // In a combo the line is drawn over the bars whatever order the measures
1192
+ // come in; at the same z a later bar series would paint across it.
1193
+ ...(combo ? { z: 3 } : {}),
1166
1194
  };
1167
1195
  }
1168
1196
  function buildPieOption(component, data, donut) {
@@ -3882,7 +3910,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3882
3910
  // complete synchronously after setOption.
3883
3911
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
3884
3912
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
3885
- const echarts = (await import('./firestitch-report-echarts-Dg5PjbjT.mjs')).default;
3913
+ const echarts = (await import('./firestitch-report-echarts-CFkoNvo_.mjs')).default;
3886
3914
  const host = document.createElement('div');
3887
3915
  host.style.width = `${width}px`;
3888
3916
  host.style.height = `${height}px`;
@@ -4416,7 +4444,7 @@ class ReportPptxService {
4416
4444
  const legendPosition = colorBy
4417
4445
  ? 'hidden'
4418
4446
  : resolveLegendPosition(config, extracted.series.length);
4419
- slide.addChart(typeMap[chartType], chartData, {
4447
+ const options = {
4420
4448
  x, y, w, h,
4421
4449
  chartColors: REPORT_CHART_COLORS,
4422
4450
  showLegend: legendPosition !== 'hidden',
@@ -4434,7 +4462,36 @@ class ReportPptxService {
4434
4462
  valAxisTitle: config.measures?.[0]?.label,
4435
4463
  valAxisTitleFontSize: 10,
4436
4464
  dataBorder: { pt: 0, color: 'FFFFFF' },
4437
- });
4465
+ };
4466
+ // The type each chartData entry is drawn as. A pie is one entry of its own
4467
+ // type; colorBy collapsed its series into one, drawn as the lone measure's.
4468
+ const dataTypes = pie
4469
+ ? [chartType]
4470
+ : seriesTypes(extracted, chartType).slice(0, colorBy ? 1 : undefined);
4471
+ this._addChart(slide, typeMap, dataTypes.length ? dataTypes : [chartType], chartData, options);
4472
+ }
4473
+ // One native chart when every entry is drawn as the same type. A combo is
4474
+ // pptxgenjs's multi-type chart: one entry per type, all on the same category
4475
+ // and value axes. It restarts the palette for every type, so each type is
4476
+ // handed the colors its series have on screen.
4477
+ _addChart(slide, typeMap, dataTypes, chartData, options) {
4478
+ const distinct = [...new Set(dataTypes)];
4479
+ if (distinct.length === 1) {
4480
+ slide.addChart(typeMap[distinct[0]], chartData, options);
4481
+ return;
4482
+ }
4483
+ slide.addChart(distinct.map((type) => {
4484
+ const indexes = dataTypes
4485
+ .map((dataType, index) => (dataType === type ? index : -1))
4486
+ .filter((index) => index >= 0);
4487
+ return {
4488
+ type: typeMap[type],
4489
+ data: indexes.map((index) => chartData[index]),
4490
+ options: {
4491
+ chartColors: indexes.map((index) => REPORT_CHART_COLORS[index % REPORT_CHART_COLORS.length]),
4492
+ },
4493
+ };
4494
+ }), options);
4438
4495
  }
4439
4496
  _renderTable(slide, component, data, x, y, w, h) {
4440
4497
  const config = component.config;
@@ -4970,7 +5027,7 @@ class ReportComponent {
4970
5027
  ReportExportService,
4971
5028
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
4972
5029
  // this route's chunk.
4973
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Dg5PjbjT.mjs').then((module) => module.default) }),
5030
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CFkoNvo_.mjs').then((module) => module.default) }),
4974
5031
  ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div\n #split\n class=\"report fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n style=\"min-height: 500px;\"\n basePath=\"reports\"\n [requestData]=\"{ reportId: selected?.id ?? null }\"\n [introMessage]=\"introMessage\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n <div class=\"viewer fs-flex fs-column\">\n <div class=\"fs-row.align-center.gap-sm\">\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [fetch]=\"fetchReports\"\n [(ngModel)]=\"selected\"\n [disabled]=\"loadingReports\"\n [padless]=\"true\"\n [multiple]=\"false\"\n [fetchOnFocus]=\"true\"\n (ngModelChange)=\"reportChange($event)\"\n placeholder=\"Report\"\n name=\"report\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n <ng-template\n fsAutocompleteChipsStatic\n (click)=\"createReport()\">\n Create Report\n </ng-template>\n </fs-autocomplete-chips>\n @if (report && editMode) {\n <!-- What layout editing can add or switch to, and the way out of it \u2014\n the same outlined buttons as the fs-ai-report toolbar. -->\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"master-toggle\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n }\n @if (report) {\n <fs-menu>\n <ng-template\n fs-menu-item\n (click)=\"reportSettings()\">\n <mat-icon>\n tune\n </mat-icon>\n Report settings\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleEditMode()\"\n [disabled]=\"editMode\">\n <mat-icon>\n dashboard_customize\n </mat-icon>\n {{ editMode ? 'Done editing layout' : 'Edit layout' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n <mat-icon>\n slideshow\n </mat-icon>\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n <mat-icon>\n picture_as_pdf\n </mat-icon>\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n }\n </div>\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the menu\n above). fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the block\n is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\">\n </app-report-canvas>\n }\n </div>\n</div>", styles: [".report{height:100%;min-height:500px}.report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:500px;min-width:0;border:none}.report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.report .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.report .resizer:before{content:\"\";width:0px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.report .resizer:hover:before{width:3px;background:var(--brand-primary-color)}.report.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.report.resizing .resizer:before{width:3px;background:var(--brand-primary-color)}.report.resizing .chat,.report.resizing .viewer{pointer-events:none}::ng-deep body.body-report-reports .mat-mdc-card-content{display:flex;flex-direction:column;box-sizing:border-box}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel{flex:1;min-height:0}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel router-outlet+ng-component{height:100%}.master-toggle.active{background:#eceff3}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$3.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$3.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$3.FsAutocompleteChipsStaticDirective, selector: "[fsAutocompleteChipsStatic]", inputs: ["show", "disable"], outputs: ["click", "selected"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$1.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$1.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4975
5032
  }
4976
5033
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
@@ -4985,7 +5042,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4985
5042
  ReportExportService,
4986
5043
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
4987
5044
  // this route's chunk.
4988
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Dg5PjbjT.mjs').then((module) => module.default) }),
5045
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CFkoNvo_.mjs').then((module) => module.default) }),
4989
5046
  ], imports: [
4990
5047
  FormsModule,
4991
5048
  FsAutocompleteChipsModule,
@@ -5662,17 +5719,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
5662
5719
  // report in no folder is a root node, which is where every report sat before
5663
5720
  // folders existed, so nothing had to be migrated or bucketed as "uncategorized".
5664
5721
  //
5665
- // Reports are soft-deletable: the row kebab deletes, "Show Deleted" flips the
5666
- // listing into deleted mode (the backend hides deleted reports otherwise), and
5667
- // in that mode the kebab offers Restore instead. Folders do NOT take part in
5668
- // that — a folder is only deletable once it is empty, so it hard deletes and has
5669
- // nothing to restore. The upshot is that deleted mode keeps the folder structure
5670
- // on screen with the deleted reports sitting where they belong, so restoring one
5671
- // puts it back rather than dropping it at the root.
5722
+ // Folders and reports are both soft-deletable, and a folder deletes whatever it
5723
+ // holds. Only the folder's own row is written: everything under it drops out of
5724
+ // the listing because it sits under a deleted folder, and comes back with it.
5725
+ // "Show Deleted" shows the WHOLE tree — live and deleted together, each deleted
5726
+ // row where it was filed — and on anything deleted the kebab offers Restore
5727
+ // instead. Restoring also restores any deleted folder above the row, so it lands
5728
+ // back where it was rather than at the root.
5729
+ //
5730
+ // Every folder starts collapsed; the tree remembers what was opened across its
5731
+ // own reloads (a rename, a move, the Show Deleted toggle) and across a search.
5672
5732
  class FsAiReportsComponent {
5673
- // How deep the tree opens on load. Folders are audience buckets, not a
5674
- // filesystem, so there is nothing worth hiding behind a closed chevron.
5675
- static EXPAND_LEVEL = 10;
5676
5733
  // API base for the reports endpoints; override to mount elsewhere.
5677
5734
  basePath = input('reports');
5678
5735
  // Where a row links to; ':id' is replaced with the report id. Default takes
@@ -5696,6 +5753,10 @@ class FsAiReportsComponent {
5696
5753
  _destroyRef = inject(DestroyRef);
5697
5754
  // The filter bar's current query — carries `state` when Show Deleted is on.
5698
5755
  _query = {};
5756
+ // The folders that were open when a search started; null while no search is
5757
+ // running. The search opens every match's path over the top of them, and
5758
+ // clearing it puts these back.
5759
+ _expandedBeforeSearch = null;
5699
5760
  constructor() {
5700
5761
  effect(() => {
5701
5762
  this._reportData.basePath = this.basePath();
@@ -5713,11 +5774,6 @@ class FsAiReportsComponent {
5713
5774
  openLink(report) {
5714
5775
  return this._patternCommands(this.openPattern(), report.id);
5715
5776
  }
5716
- // A deleted report can't be opened — the backend only assembles active ones —
5717
- // so in Show Deleted mode its name renders as plain text rather than a link.
5718
- isDeleted(report) {
5719
- return report?.state === ReportState.Deleted;
5720
- }
5721
5777
  // Prompt for a title, create the report — filed under reportFolderId when a
5722
5778
  // folder's kebab asked, at the root otherwise — then route to it (createPattern).
5723
5779
  createReport(reportFolderId = null) {
@@ -5752,13 +5808,16 @@ class FsAiReportsComponent {
5752
5808
  type: ItemType.Keyword,
5753
5809
  label: 'Search',
5754
5810
  },
5811
+ // A checkbox rather than the usual Status select with a "Show Deleted"
5812
+ // isolate. Isolating the deleted rows would strand them: a deleted
5813
+ // report in a live folder arrives without the folder it sits in, and a
5814
+ // tree cannot place a node whose parent it never received. So the
5815
+ // toggle asks for everything, and the rows mark what is deleted.
5755
5816
  {
5756
5817
  name: 'state',
5757
- type: ItemType.Select,
5758
- label: 'Status',
5759
- multiple: true,
5760
- values: () => ReportStates,
5761
- isolate: { label: 'Show Deleted', value: ReportState.Deleted },
5818
+ type: ItemType.Checkbox,
5819
+ label: 'Show Deleted',
5820
+ checked: [ReportState.Active, ReportState.Deleted].join(','),
5762
5821
  },
5763
5822
  ],
5764
5823
  actions: [
@@ -5791,15 +5850,19 @@ class FsAiReportsComponent {
5791
5850
  click: () => this.createReport(),
5792
5851
  },
5793
5852
  ],
5794
- // The two items are answered in different places and both have to run.
5795
- // The keyword drives the tree's own search, which is the whole point of
5796
- // searching a tree — it reveals a match nested inside collapsed folders
5797
- // rather than only filtering what is already on screen. `state` is the
5798
- // Show Deleted toggle, and only the server can answer that.
5853
+ // The two items are answered in different places, and each only when it
5854
+ // changed. The keyword drives the tree's own search, which is the whole
5855
+ // point of searching a tree — it reveals a match nested inside collapsed
5856
+ // folders rather than only filtering what is already on screen. `state` is
5857
+ // the Show Deleted toggle, and only the server can answer that. Running the
5858
+ // search on a toggle would clear it, and clearing collapses the tree.
5799
5859
  change: (query) => {
5860
+ const keywordChanged = String(query?.keyword ?? '') !== String(this._query.keyword ?? '');
5800
5861
  const stateChanged = String(query?.state ?? '') !== String(this._query.state ?? '');
5801
5862
  this._query = { ...query };
5802
- this._search(query?.keyword);
5863
+ if (keywordChanged) {
5864
+ this._search(query?.keyword);
5865
+ }
5803
5866
  if (stateChanged) {
5804
5867
  this._load();
5805
5868
  }
@@ -5807,22 +5870,26 @@ class FsAiReportsComponent {
5807
5870
  };
5808
5871
  }
5809
5872
  _initTreeConfig() {
5873
+ // No expandLevel: every folder starts collapsed.
5810
5874
  this.treeConfig = {
5811
5875
  data: [],
5812
5876
  childrenName: 'children',
5813
- expandLevel: FsAiReportsComponent.EXPAND_LEVEL,
5814
5877
  draggable: true,
5815
5878
  // The whole row is the grip and the handle stays hidden; a press only
5816
5879
  // becomes a drag once the pointer moves, so the kebab, the chevron and the
5817
5880
  // report link all stay clickable.
5818
5881
  dragMode: TreeDragMode.Node,
5882
+ // Anything deleted is restored before it is moved — the backend refuses
5883
+ // to edit a deleted row, and a drag would only snap back.
5884
+ canDrag: (node) => !this._isDeletedNode(node),
5819
5885
  // canDrop is asked about the node being DRAGGED, so only `toParent`
5820
5886
  // describes the destination — reading node.level/node.parent here would
5821
5887
  // describe where it came from. A report can never be a parent because its
5822
5888
  // type says so, and that single test is the whole "folders nest, reports
5823
- // are leaves" rule on this side.
5889
+ // are leaves" rule on this side. A deleted folder is no destination
5890
+ // either: whatever landed in it would vanish from the live listing.
5824
5891
  canDrop: (node, fromParent, toParent) => {
5825
- return !toParent || toParent.data?.type === ReportNodeType.Folder;
5892
+ return !toParent || (this._isFolderNode(toParent) && !this._isDeletedNode(toParent));
5826
5893
  },
5827
5894
  changeReorder: (event) => this._move(event),
5828
5895
  nodeClass: (node) => node.data?.type,
@@ -5846,12 +5913,12 @@ class FsAiReportsComponent {
5846
5913
  },
5847
5914
  {
5848
5915
  label: 'New report',
5849
- show: (node) => this._isFolderNode(node),
5916
+ show: (node) => this._isFolderNode(node) && !this._isDeletedNode(node),
5850
5917
  click: (node) => this.createReport(node.data?.id),
5851
5918
  },
5852
5919
  {
5853
5920
  label: 'New folder',
5854
- show: (node) => this._isFolderNode(node),
5921
+ show: (node) => this._isFolderNode(node) && !this._isDeletedNode(node),
5855
5922
  click: (node) => this._createFolder(node.data?.id),
5856
5923
  },
5857
5924
  {
@@ -5859,8 +5926,8 @@ class FsAiReportsComponent {
5859
5926
  show: (node) => !this._isDeletedNode(node),
5860
5927
  click: (node) => this._delete(node),
5861
5928
  },
5862
- // Folders never reach this: they hard delete, so a deleted folder
5863
- // does not exist to be restored.
5929
+ // On every deleted row, including a live one that is only hidden by
5930
+ // a deleted folder above it — restoring that brings the folder back.
5864
5931
  {
5865
5932
  label: 'Restore',
5866
5933
  show: (node) => this._isDeletedNode(node),
@@ -5875,66 +5942,128 @@ class FsAiReportsComponent {
5875
5942
  return node?.data?.type === ReportNodeType.Folder;
5876
5943
  }
5877
5944
  _isDeletedNode(node) {
5878
- return this.isDeleted(node?.data?.report);
5945
+ return !!node?.data?.deleted;
5879
5946
  }
5880
5947
  // Drive the tree's built-in search: it collapses everything, hides every node,
5881
5948
  // then unhides each match and walks its ancestors open, so a report several
5882
- // folders deep surfaces with its path expanded. Clearing does NOT undo that
5883
- // initial collapse, so the tree is re-opened here — otherwise the listing is
5884
- // left fully collapsed the moment somebody empties the search box.
5949
+ // folders deep surfaces with its path expanded. Clearing collapses everything
5950
+ // again, so the folders that were open before the search are put back.
5885
5951
  _search(keyword) {
5886
5952
  const tree = this._tree();
5887
5953
  if (!tree) {
5888
5954
  return;
5889
5955
  }
5890
5956
  if (keyword) {
5957
+ this._expandedBeforeSearch ??= this._expandedKeys();
5891
5958
  tree.search(keyword);
5892
5959
  return;
5893
5960
  }
5894
5961
  tree.clearSearch();
5895
- tree.expandAll();
5962
+ this._expandKeys(this._expandedBeforeSearch ?? []);
5963
+ this._expandedBeforeSearch = null;
5896
5964
  }
5897
5965
  _load() {
5898
- this._reportData.tree(this._query)
5966
+ // Only the Show Deleted toggle is the server's to answer — the keyword is
5967
+ // the tree's own search — and an unchecked toggle sends nothing, leaving the
5968
+ // backend's active-only default in charge.
5969
+ const query = this._query.state ? { state: this._query.state } : {};
5970
+ this._reportData.tree(query)
5899
5971
  .pipe(takeUntilDestroyed(this._destroyRef))
5900
- .subscribe((tree) => {
5901
- const nodes = this._buildNodes(tree);
5972
+ .subscribe((reportTree) => {
5973
+ const nodes = this._buildNodes(reportTree);
5902
5974
  this.nodes.set(nodes);
5903
5975
  // The tree reads `config` once, in its own ngOnInit. The first load
5904
5976
  // lands before it exists, so the data has to arrive through the config;
5905
5977
  // every later one has to go through the instance or nothing changes.
5906
5978
  this.treeConfig = { ...this.treeConfig, data: nodes };
5907
- this._tree()?.setData(nodes);
5979
+ const tree = this._tree();
5980
+ if (!tree) {
5981
+ return;
5982
+ }
5983
+ // setData() rebuilds every node collapsed, so a rename or a move would
5984
+ // otherwise slam shut every folder somebody had opened. Keys are stable
5985
+ // across loads ('f:12'), so what was open is reopened by key — and a
5986
+ // folder that no longer comes back simply is not.
5987
+ const expandedKeys = this._expandedKeys();
5988
+ tree.setData(nodes);
5989
+ this._expandKeys(expandedKeys);
5990
+ const keyword = String(this._query.keyword ?? '');
5991
+ if (keyword) {
5992
+ tree.search(keyword);
5993
+ }
5908
5994
  });
5909
5995
  }
5996
+ // The keys of every open folder, walked through the tree's public node API.
5997
+ _expandedKeys() {
5998
+ const tree = this._tree();
5999
+ if (!tree) {
6000
+ return [];
6001
+ }
6002
+ const walk = (parent) => tree.getNodes(parent)
6003
+ .filter((node) => node.isExpanded())
6004
+ .flatMap((node) => [node.data?.key, ...walk(node)]);
6005
+ return walk(null);
6006
+ }
6007
+ _expandKeys(keys) {
6008
+ const tree = this._tree();
6009
+ if (!tree || !keys.length) {
6010
+ return;
6011
+ }
6012
+ const open = new Set(keys);
6013
+ const walk = (parent) => {
6014
+ tree.getNodes(parent)
6015
+ .filter((node) => open.has(node.data?.key))
6016
+ .forEach((node) => {
6017
+ node.expand();
6018
+ walk(node);
6019
+ });
6020
+ };
6021
+ walk(null);
6022
+ }
5910
6023
  // Flat API rows → the nested shape fs-tree renders. Folders come before
5911
6024
  // reports at every level, each already name-ordered by the backend.
5912
- _buildNodes(tree) {
5913
- const folders = tree?.folders ?? [];
5914
- const reports = tree?.reports ?? [];
5915
- const build = (parentId) => {
6025
+ //
6026
+ // `deleted` is inherited downward: a deleted folder hides everything below it
6027
+ // without the rows below being deleted themselves, and that is only visible
6028
+ // from here, where the parent is in hand. The backend applies the same rule
6029
+ // when it filters (ReportFolderModel::getDeletedFolderIds()).
6030
+ _buildNodes(reportTree) {
6031
+ const folders = reportTree?.folders ?? [];
6032
+ const reports = reportTree?.reports ?? [];
6033
+ const build = (parentId, parentDeleted) => {
5916
6034
  const folderNodes = folders
5917
6035
  .filter((folder) => (folder.parentReportFolderId ?? null) === parentId)
5918
- .map((folder) => ({
5919
- key: `f:${folder.id}`,
5920
- id: folder.id,
5921
- name: folder.name,
5922
- type: ReportNodeType.Folder,
5923
- children: build(folder.id),
5924
- }));
6036
+ .map((folder) => {
6037
+ const stateDeleted = folder.state === ReportState.Deleted;
6038
+ const deleted = parentDeleted || stateDeleted;
6039
+ return {
6040
+ key: `f:${folder.id}`,
6041
+ id: folder.id,
6042
+ name: folder.name,
6043
+ type: ReportNodeType.Folder,
6044
+ children: build(folder.id, deleted),
6045
+ stateDeleted,
6046
+ deleted,
6047
+ };
6048
+ });
5925
6049
  const reportNodes = reports
5926
6050
  .filter((report) => (report.reportFolderId ?? null) === parentId)
5927
- .map((report) => ({
5928
- key: `r:${report.id}`,
5929
- id: report.id,
5930
- name: report.name,
5931
- type: ReportNodeType.Report,
5932
- children: [],
5933
- report,
5934
- }));
6051
+ .map((report) => {
6052
+ const stateDeleted = report.state === ReportState.Deleted;
6053
+ return {
6054
+ key: `r:${report.id}`,
6055
+ id: report.id,
6056
+ name: report.name,
6057
+ type: ReportNodeType.Report,
6058
+ children: [],
6059
+ report,
6060
+ stateDeleted,
6061
+ deleted: parentDeleted || stateDeleted,
6062
+ };
6063
+ });
5935
6064
  return [...folderNodes, ...reportNodes];
5936
6065
  };
5937
- return build(null);
6066
+ return build(null, false);
5938
6067
  }
5939
6068
  // A drop moved a node under a new parent, or out to the root. The tree has
5940
6069
  // already moved it on screen, so both outcomes reload: a success to pick up
@@ -5995,9 +6124,9 @@ class FsAiReportsComponent {
5995
6124
  this._load();
5996
6125
  });
5997
6126
  }
5998
- // Deleting a folder is refused server-side while it still holds a sub-folder
5999
- // or an active report, and the message says which — so the confirm here warns
6000
- // that a folder delete is permanent rather than trying to restate the rule.
6127
+ // A folder deletes with everything in it, so the confirm says what that is —
6128
+ // and that it all comes back from Show Deleted, which is what makes it safe to
6129
+ // offer.
6001
6130
  _delete(node) {
6002
6131
  const data = node?.data;
6003
6132
  if (!data) {
@@ -6007,7 +6136,7 @@ class FsAiReportsComponent {
6007
6136
  this._prompt.confirm({
6008
6137
  title: 'Confirm',
6009
6138
  template: folder
6010
- ? `Are you sure you would like to delete the folder "${data.name}"? This cannot be undone.`
6139
+ ? this._deleteFolderMessage(data)
6011
6140
  : 'Are you sure you would like to delete this report?',
6012
6141
  })
6013
6142
  .pipe(switchMap(() => (folder
@@ -6018,15 +6147,41 @@ class FsAiReportsComponent {
6018
6147
  this._load();
6019
6148
  });
6020
6149
  }
6150
+ // Counted from the node already on screen, live rows only: in Show Deleted
6151
+ // mode the folder can also hold rows that are gone already, and those are not
6152
+ // leaving with it.
6153
+ _deleteFolderMessage(folder) {
6154
+ const count = (node) => node.children
6155
+ .filter((child) => !child.deleted)
6156
+ .reduce((total, child) => {
6157
+ const below = count(child);
6158
+ return {
6159
+ folders: total.folders + below.folders + (child.type === ReportNodeType.Folder ? 1 : 0),
6160
+ reports: total.reports + below.reports + (child.type === ReportNodeType.Report ? 1 : 0),
6161
+ };
6162
+ }, { folders: 0, reports: 0 });
6163
+ const { folders, reports } = count(folder);
6164
+ const contents = [
6165
+ folders ? `${folders} ${folders === 1 ? 'folder' : 'folders'}` : '',
6166
+ reports ? `${reports} ${reports === 1 ? 'report' : 'reports'}` : '',
6167
+ ].filter(Boolean);
6168
+ const inside = contents.length ? ` and the ${contents.join(' and ')} inside it` : '';
6169
+ return `Are you sure you would like to delete the folder "${folder.name}"${inside}? It can be restored from Show Deleted.`;
6170
+ }
6171
+ // Both endpoints also restore any deleted folder above the row, so a row that
6172
+ // is live itself but hidden by a deleted folder is restored by the same call.
6021
6173
  _restore(node) {
6022
6174
  const data = node?.data;
6023
6175
  if (!data) {
6024
6176
  return;
6025
6177
  }
6026
- this._reportData.undelete(data.id)
6178
+ const folder = data.type === ReportNodeType.Folder;
6179
+ (folder
6180
+ ? this._reportData.undeleteFolder(data.id)
6181
+ : this._reportData.undelete(data.id))
6027
6182
  .pipe(takeUntilDestroyed(this._destroyRef))
6028
6183
  .subscribe(() => {
6029
- this._message.success('Restored report');
6184
+ this._message.success(folder ? 'Restored folder' : 'Restored report');
6030
6185
  this._load();
6031
6186
  });
6032
6187
  }
@@ -6046,17 +6201,18 @@ class FsAiReportsComponent {
6046
6201
  return segments;
6047
6202
  }
6048
6203
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6049
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "_tree", first: true, predicate: FsTreeComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div class=\"fs-row.gap-sm.align-center node-row\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder) {\n <span class=\"node-name\">{{ data.name }}</span>\n } @else if (isDeleted(data.report)) {\n <!-- A deleted report can't be opened (the backend only assembles\n active ones), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i3$3.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i3$3.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6204
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "_tree", first: true, predicate: FsTreeComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div\n class=\"fs-row.gap-sm.align-center node-row\"\n [class.deleted]=\"data.deleted\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder || data.deleted) {\n <!-- A deleted report can't be opened (the backend only assembles\n live ones, and one under a deleted folder is not on the live\n listing), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n\n <!-- Only on the row somebody deleted. Everything under a deleted\n folder is muted with it, but was not deleted itself. -->\n @if (data.stateDeleted) {\n <fs-chip size=\"tiny\">Deleted</fs-chip>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-row.deleted{opacity:.55}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"], dependencies: [{ kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1$4.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i4$1.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i4$1.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6050
6205
  }
6051
6206
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
6052
6207
  type: Component,
6053
6208
  args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
6209
+ FsChipModule,
6054
6210
  FsFilterModule,
6055
6211
  FsSkeletonModule,
6056
6212
  FsTreeModule,
6057
6213
  MatIconModule,
6058
6214
  RouterLink,
6059
- ], template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div class=\"fs-row.gap-sm.align-center node-row\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder) {\n <span class=\"node-name\">{{ data.name }}</span>\n } @else if (isDeleted(data.report)) {\n <!-- A deleted report can't be opened (the backend only assembles\n active ones), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"] }]
6215
+ ], template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div\n class=\"fs-row.gap-sm.align-center node-row\"\n [class.deleted]=\"data.deleted\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder || data.deleted) {\n <!-- A deleted report can't be opened (the backend only assembles\n live ones, and one under a deleted folder is not on the live\n listing), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n\n <!-- Only on the row somebody deleted. Everything under a deleted\n folder is muted with it, but was not deleted itself. -->\n @if (data.stateDeleted) {\n <fs-chip size=\"tiny\">Deleted</fs-chip>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-row.deleted{opacity:.55}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"] }]
6060
6216
  }], ctorParameters: () => [] });
6061
6217
 
6062
6218
  // A single report: the AI chat on the left, the report canvas on the right,
@@ -6438,8 +6594,8 @@ class FsAiReportComponent {
6438
6594
  ReportPdfService,
6439
6595
  ReportExportService,
6440
6596
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
6441
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Dg5PjbjT.mjs').then((module) => module.default) }),
6442
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode\"\n [matTooltip]=\"editMode ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n [pageId]=\"pageId\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$1.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$1.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$1.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$4.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$4.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6597
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CFkoNvo_.mjs').then((module) => module.default) }),
6598
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode\"\n [matTooltip]=\"editMode ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n [pageId]=\"pageId\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$1.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$1.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$1.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6443
6599
  }
6444
6600
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
6445
6601
  type: Component,
@@ -6452,7 +6608,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6452
6608
  ReportPdfService,
6453
6609
  ReportExportService,
6454
6610
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
6455
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-Dg5PjbjT.mjs').then((module) => module.default) }),
6611
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CFkoNvo_.mjs').then((module) => module.default) }),
6456
6612
  ], imports: [
6457
6613
  FsFilterModule,
6458
6614
  FsMenuModule,
@@ -6485,5 +6641,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6485
6641
  * Generated bundle index. Do not edit.
6486
6642
  */
6487
6643
 
6488
- export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, ReportNodeType as f, ReportState as g, ReportStates as h };
6489
- //# sourceMappingURL=firestitch-report-firestitch-report-JORFp_xa.mjs.map
6644
+ export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, ReportNodeType as f, ReportState as g };
6645
+ //# sourceMappingURL=firestitch-report-firestitch-report-PZzkYYTD.mjs.map