@firestitch/report 18.0.19 → 18.0.20

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.
@@ -13,12 +13,12 @@ import * as i2$3 from '@firestitch/menu';
13
13
  import { FsMenuModule } from '@firestitch/menu';
14
14
  import { FsProcess } from '@firestitch/process';
15
15
  import { FsPrompt } from '@firestitch/prompt';
16
- import { of, Subject, map as map$1, debounceTime, forkJoin, BehaviorSubject, from } from 'rxjs';
16
+ import { of, Subject, map as map$1, debounceTime, forkJoin, BehaviorSubject, from, switchMap as switchMap$1 } from 'rxjs';
17
17
  import { map, shareReplay, filter, tap, switchMap, catchError, finalize, take } from 'rxjs/operators';
18
18
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
19
19
  import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
20
20
  import { FsAiChatComponent } from '@firestitch/ai';
21
- import * as i3$1 from '@angular/material/button';
21
+ import * as i3$2 from '@angular/material/button';
22
22
  import { MatIconButton, MatButton, MatButtonModule } from '@angular/material/button';
23
23
  import * as i2$5 from '@angular/material/tooltip';
24
24
  import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
@@ -51,12 +51,14 @@ import { FsDatePickerModule } from '@firestitch/datepicker';
51
51
  import { MatOption } from '@angular/material/core';
52
52
  import { guid } from '@firestitch/common';
53
53
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
54
- import * as i2$4 from '@firestitch/date';
55
- import { FsDateModule } from '@firestitch/date';
56
54
  import * as i4$1 from '@firestitch/skeleton';
57
55
  import { FsSkeletonModule } from '@firestitch/skeleton';
56
+ import * as i3$1 from '@firestitch/tree';
57
+ import { FsTreeComponent, TreeDragMode, TreeActionType, FsTreeModule } from '@firestitch/tree';
58
58
  import * as i1$4 from '@firestitch/chip';
59
59
  import { FsChipModule } from '@firestitch/chip';
60
+ import * as i2$4 from '@firestitch/date';
61
+ import { FsDateModule } from '@firestitch/date';
60
62
  import { FsMarkdownRendererComponent, FsMarkdownEditorComponent } from '@firestitch/markdown-editor';
61
63
  import * as i1$5 from '@angular/material/expansion';
62
64
  import { MatExpansionModule } from '@angular/material/expansion';
@@ -80,12 +82,49 @@ class ReportData {
80
82
  ...config,
81
83
  });
82
84
  }
83
- create(name, config = {}) {
84
- return this._api.post(this._path(), { name }, {
85
+ // Both halves of the listing — reports AND the folders they are filed into —
86
+ // from the one GET. No `key`, because the whole payload is the answer here;
87
+ // reports() keeps its 'reports' key for callers that only want the flat list.
88
+ tree(query = {}, config = {}) {
89
+ return this._api.get(this._path(), query, config);
90
+ }
91
+ create(name, reportFolderId = null, config = {}) {
92
+ return this._api.post(this._path(), { name, reportFolderId }, {
85
93
  key: 'report',
86
94
  ...config,
87
95
  });
88
96
  }
97
+ // File a report under a folder, or pass null to move it to the root. Sent
98
+ // explicitly (never omitted) so the backend can tell "move to root" from
99
+ // "leave the folder alone".
100
+ moveReport(reportId, reportFolderId, config = {}) {
101
+ return this._api.put(this._path(`${reportId}`), { reportFolderId }, {
102
+ key: 'report',
103
+ ...config,
104
+ });
105
+ }
106
+ createFolder(name, parentReportFolderId = null, config = {}) {
107
+ return this._api.post(this._path('folders'), { name, parentReportFolderId }, {
108
+ key: 'folder',
109
+ ...config,
110
+ });
111
+ }
112
+ // Rename and/or move a folder. Only the keys passed are acted on server-side,
113
+ // so a rename never disturbs the parent and a drag never disturbs the name.
114
+ updateFolder(reportFolderId, folder, config = {}) {
115
+ return this._api.put(this._path(`folders/${reportFolderId}`), folder, {
116
+ key: 'folder',
117
+ ...config,
118
+ });
119
+ }
120
+ // A real delete — the backend refuses while the folder still holds a
121
+ // sub-folder or an active report, so there is no undelete counterpart.
122
+ deleteFolder(reportFolderId, config = {}) {
123
+ return this._api.delete(this._path(`folders/${reportFolderId}`), {}, {
124
+ key: 'folder',
125
+ ...config,
126
+ });
127
+ }
89
128
  rename(reportId, name, config = {}) {
90
129
  return this._api.put(this._path(`${reportId}`), { name }, {
91
130
  key: 'report',
@@ -165,6 +204,17 @@ class ReportData {
165
204
  ...config,
166
205
  });
167
206
  }
207
+ // Exports EVERY component on the report — or on one page — server-side and
208
+ // resolves to a presigned URL for a zip of one CSV per component, foldered by
209
+ // page. `filters` carries the resolved values for every component together;
210
+ // each one picks out its own. Same keep-alive contract as exportComponent,
211
+ // over a longer run; consume through FsProcess.download.
212
+ exportReport(reportId, filters = [], pageId = null, config = {}) {
213
+ return this._api.post(this._path(`${reportId}/export`), { filters, ...(pageId ? { pageId } : {}) }, {
214
+ key: 'url',
215
+ ...config,
216
+ });
217
+ }
168
218
  // Distinct options for a select filter (drives multiselect controls).
169
219
  filterOptions(reportId, filterId, config = {}) {
170
220
  return this._api.get(this._path(`${reportId}/filters/${filterId}/options`), {}, {
@@ -599,6 +649,17 @@ class ReportFilterStateService {
599
649
  }
600
650
  return resolved;
601
651
  }
652
+ // Every component's resolved values in one array — what a report-level export
653
+ // carries so each file in the archive is filtered exactly as the screen is.
654
+ // Filter ids are unique per component, so the backend picks each component's
655
+ // own out of the pile. A page id narrows it to that page.
656
+ resolveForReport(report, pageId = null) {
657
+ const pages = (report.pages ?? [])
658
+ .filter((page) => !pageId || page.id === pageId);
659
+ return pages
660
+ .flatMap((page) => (page.components ?? [])
661
+ .flatMap((component) => this.resolveForComponent(component)));
662
+ }
602
663
  // A group's authored default, mapped to a session value by type. Relative
603
664
  // date presets resolve against "now" each time the report opens, so a saved
604
665
  // "last 12 months" stays relative; a fixed range, select values and keyword
@@ -2528,13 +2589,24 @@ const FREQUENCY_OPTIONS = [
2528
2589
  { value: 'quarter', name: 'Quarter' },
2529
2590
  { value: 'year', name: 'Annually' },
2530
2591
  ];
2592
+ var ReportNodeType;
2593
+ (function (ReportNodeType) {
2594
+ ReportNodeType["Folder"] = "folder";
2595
+ ReportNodeType["Report"] = "report";
2596
+ })(ReportNodeType || (ReportNodeType = {}));
2531
2597
  // Reports are soft-deletable: a deleted report leaves the listing but keeps its
2532
2598
  // pages and components, and the listing's "Show Deleted" mode restores it.
2599
+ // Folders have no equivalent — they are only deletable once empty, so they hard
2600
+ // delete and there is nothing to restore.
2533
2601
  var ReportState;
2534
2602
  (function (ReportState) {
2535
2603
  ReportState["Active"] = "active";
2536
2604
  ReportState["Deleted"] = "deleted";
2537
2605
  })(ReportState || (ReportState = {}));
2606
+ const ReportStates = [
2607
+ { name: 'Active', value: ReportState.Active },
2608
+ { name: 'Deleted', value: ReportState.Deleted },
2609
+ ];
2538
2610
 
2539
2611
  // Rows fetched for a list component's export table — enough to fill a
2540
2612
  // slide/page table; the cap note covers the rest.
@@ -2648,7 +2720,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2648
2720
  // complete synchronously after setOption.
2649
2721
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
2650
2722
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
2651
- const echarts = (await import('./firestitch-report-echarts-CfwgLaOA.mjs')).default;
2723
+ const echarts = (await import('./firestitch-report-echarts-D4VLD05J.mjs')).default;
2652
2724
  const host = document.createElement('div');
2653
2725
  host.style.width = `${width}px`;
2654
2726
  host.style.height = `${height}px`;
@@ -3122,6 +3194,9 @@ class ReportComponent {
3122
3194
  reportFilterConfig;
3123
3195
  _split;
3124
3196
  _chatPanel;
3197
+ // Only the canvas knows which page is on screen, and a page-scoped export
3198
+ // has to ask it.
3199
+ _canvas;
3125
3200
  introMessage = {
3126
3201
  text: '',
3127
3202
  };
@@ -3254,6 +3329,19 @@ class ReportComponent {
3254
3329
  this._process.run('Exporting PDF', this._exportCollector.collect(report)
3255
3330
  .pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
3256
3331
  }
3332
+ // The raw data behind the whole report in one action: the server exports every
3333
+ // component's full filtered dataset and zips one CSV per component, foldered
3334
+ // by page. `thisPage` narrows it to the page the canvas is showing. Unlike the
3335
+ // PDF/PowerPoint exports this is entirely server-side — nothing is collected
3336
+ // in the browser, so it is not capped by what a slide can hold.
3337
+ exportData(thisPage = false) {
3338
+ const report = this.report;
3339
+ if (!report) {
3340
+ return;
3341
+ }
3342
+ const pageId = thisPage ? this._canvas?.activePage?.id ?? null : null;
3343
+ this._process.download('Exporting data', this._reportData.exportReport(report.id, this._filterState.resolveForReport(report, pageId), pageId));
3344
+ }
3257
3345
  // React to each chat turn (fs-chat handles the request + persistence); we
3258
3346
  // only care about the report side effects this context's backend flags.
3259
3347
  onChatResponse(response) {
@@ -3394,8 +3482,8 @@ class ReportComponent {
3394
3482
  ReportPdfService,
3395
3483
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3396
3484
  // this route's chunk.
3397
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
3398
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: 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) {\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_2_edit\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 </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 (editDone)=\"toggleEditMode()\">\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%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.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$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$2.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$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3485
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
3486
+ ], 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) {\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_2_edit\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 (editDone)=\"toggleEditMode()\">\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%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.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$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$2.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$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3399
3487
  }
3400
3488
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
3401
3489
  type: Component,
@@ -3408,7 +3496,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3408
3496
  ReportPdfService,
3409
3497
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3410
3498
  // this route's chunk.
3411
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
3499
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
3412
3500
  ], imports: [
3413
3501
  FormsModule,
3414
3502
  FsAutocompleteChipsModule,
@@ -3417,13 +3505,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3417
3505
  FsAiChatComponent,
3418
3506
  MatIcon,
3419
3507
  ReportCanvasComponent,
3420
- ], 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) {\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_2_edit\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 </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 (editDone)=\"toggleEditMode()\">\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%}\n"] }]
3508
+ ], 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) {\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_2_edit\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 (editDone)=\"toggleEditMode()\">\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%}\n"] }]
3421
3509
  }], ctorParameters: () => [], propDecorators: { _split: [{
3422
3510
  type: ViewChild,
3423
3511
  args: ['split', { static: true }]
3424
3512
  }], _chatPanel: [{
3425
3513
  type: ViewChild,
3426
3514
  args: ['chatPanel', { static: true, read: ElementRef }]
3515
+ }], _canvas: [{
3516
+ type: ViewChild,
3517
+ args: [ReportCanvasComponent]
3427
3518
  }] } });
3428
3519
 
3429
3520
  // The AI Knowledge document's version history (append-only), each version
@@ -4050,18 +4141,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4050
4141
  ], template: "<form\n *fsSkeletonForm=\"loaded()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n <mat-tab\n label=\"General\"\n name=\"general\">\n <fs-ai-report-global-settings-general\n [concurrency]=\"concurrency()\"\n (concurrencyChange)=\"concurrency.set($event)\"\n [min]=\"settings()!.semanticBuildConcurrencyMin\"\n [max]=\"settings()!.semanticBuildConcurrencyMax\">\n </fs-ai-report-global-settings-general>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); General uses the default Save/Cancel; the\n Semantic Database tab is a read/run surface, so it gets a primary\n Close plus its Build/Rebuild/Stop action. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'general'\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n @if (db.status(); as buildStatus) {\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ buildStatus.detail.generatedAt ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"] }]
4051
4142
  }], ctorParameters: () => [] });
4052
4143
 
4053
- // The reports listing — an fs-list of reports. Rows are links built from a URL
4054
- // pattern (a ':id' token → the report id; a leading '/' makes it absolute,
4055
- // otherwise it's relative to this component's route). Create prompts for a
4056
- // title, creates the report, then routes to it. The header gear opens the
4057
- // global Report Settings dialog (AI knowledge document + semantic-database
4058
- // build), which lives in this package. No routing is hard-coded here.
4144
+ // The reports listing — an fs-tree of folders and the reports filed into them.
4145
+ // Rows link out through a URL pattern (a ':id' token → the report id; a leading
4146
+ // '/' makes it absolute, otherwise it's relative to this component's route).
4147
+ // Create prompts for a title, creates the report, then routes to it. The gear
4148
+ // opens the global Report Settings dialog (AI knowledge document +
4149
+ // semantic-database build), which lives in this package. No routing is
4150
+ // hard-coded here.
4151
+ //
4152
+ // Folders nest; reports are always leaves — canDrop enforces it here, and the
4153
+ // backend enforces it structurally by keeping the tree in its own table. A
4154
+ // report in no folder is a root node, which is where every report sat before
4155
+ // folders existed, so nothing had to be migrated or bucketed as "uncategorized".
4059
4156
  //
4060
4157
  // Reports are soft-deletable: the row kebab deletes, "Show Deleted" flips the
4061
- // list into deleted mode (the backend hides deleted reports otherwise), and in
4062
- // that mode fs-list swaps Delete for Restore, which calls the undelete action
4063
- // endpoint.
4158
+ // listing into deleted mode (the backend hides deleted reports otherwise), and
4159
+ // in that mode the kebab offers Restore instead. Folders do NOT take part in
4160
+ // that — a folder is only deletable once it is empty, so it hard deletes and has
4161
+ // nothing to restore. The upshot is that deleted mode keeps the folder structure
4162
+ // on screen with the deleted reports sitting where they belong, so restoring one
4163
+ // puts it back rather than dropping it at the root.
4064
4164
  class FsAiReportsComponent {
4165
+ // How deep the tree opens on load. Folders are audience buckets, not a
4166
+ // filesystem, so there is nothing worth hiding behind a closed chevron.
4167
+ static EXPAND_LEVEL = 10;
4065
4168
  // API base for the reports endpoints; override to mount elsewhere.
4066
4169
  basePath = input('reports');
4067
4170
  // Where a row links to; ':id' is replaced with the report id. Default takes
@@ -4069,27 +4172,44 @@ class FsAiReportsComponent {
4069
4172
  openPattern = input(':id');
4070
4173
  // Where a freshly-created report routes to; same ':id' pattern rules.
4071
4174
  createPattern = input(':id');
4072
- list = null;
4073
- listConfig;
4074
- // Template-side handle on the enum, so the name cell can tell a deleted row
4075
- // (shown in Show Deleted mode) from a live one.
4076
- deletedState = ReportState.Deleted;
4175
+ // Null until the first load resolves — that is what gates *fsSkeleton.
4176
+ nodes = signal(null);
4177
+ treeConfig;
4178
+ filterConfig;
4179
+ // Template-side handle on the enum, so a row can tell a folder from a report.
4180
+ nodeType = ReportNodeType;
4181
+ _tree = viewChild(FsTreeComponent);
4077
4182
  _reportData = inject(ReportData);
4078
4183
  _router = inject(Router);
4079
4184
  _route = inject(ActivatedRoute);
4080
4185
  _prompt = inject(FsPrompt);
4186
+ _message = inject(FsMessage);
4081
4187
  _dialog = inject(MatDialog);
4082
4188
  _destroyRef = inject(DestroyRef);
4189
+ // The filter bar's current query — carries `state` when Show Deleted is on.
4190
+ _query = {};
4083
4191
  constructor() {
4084
4192
  effect(() => {
4085
4193
  this._reportData.basePath = this.basePath();
4086
4194
  });
4087
- this._initListConfig();
4195
+ }
4196
+ ngOnInit() {
4197
+ // Set before the first fetch rather than waiting on the effect above, which
4198
+ // does not run until after the initial change detection pass.
4199
+ this._reportData.basePath = this.basePath();
4200
+ this._initFilterConfig();
4201
+ this._initTreeConfig();
4202
+ this._load();
4088
4203
  }
4089
4204
  // routerLink commands for a report row (built from openPattern).
4090
4205
  openLink(report) {
4091
4206
  return this._patternCommands(this.openPattern(), report.id);
4092
4207
  }
4208
+ // A deleted report can't be opened — the backend only assembles active ones —
4209
+ // so in Show Deleted mode its name renders as plain text rather than a link.
4210
+ isDeleted(report) {
4211
+ return report?.state === ReportState.Deleted;
4212
+ }
4093
4213
  // Prompt for a title, create the report, then route to it (createPattern).
4094
4214
  createReport() {
4095
4215
  this._prompt.input({
@@ -4098,27 +4218,12 @@ class FsAiReportsComponent {
4098
4218
  commitLabel: 'Create',
4099
4219
  required: true,
4100
4220
  })
4101
- .pipe(switchMap((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
4221
+ .pipe(switchMap$1((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
4102
4222
  .subscribe((report) => {
4103
4223
  const pattern = this.createPattern();
4104
4224
  this._router.navigate(this._patternCommands(pattern, report.id), pattern.startsWith('/') ? {} : { relativeTo: this._route });
4105
4225
  });
4106
4226
  }
4107
- // Turn a ':id' pattern into router commands. Relative unless it starts
4108
- // with '/', in which case the first segment carries the leading slash.
4109
- _patternCommands(pattern, id) {
4110
- const absolute = pattern.startsWith('/');
4111
- const segments = pattern.split('/')
4112
- .filter((segment) => segment.length > 0)
4113
- .map((segment) => (segment === ':id' ? id : segment));
4114
- if (!segments.length) {
4115
- return [id];
4116
- }
4117
- if (absolute) {
4118
- segments[0] = `/${segments[0]}`;
4119
- }
4120
- return segments;
4121
- }
4122
4227
  // The global Report Settings dialog: the AI knowledge document and the
4123
4228
  // semantic-database build. A running build streams into the FsProcess dock,
4124
4229
  // which survives the dialog closing.
@@ -4130,66 +4235,315 @@ class FsAiReportsComponent {
4130
4235
  autoFocus: false,
4131
4236
  });
4132
4237
  }
4133
- _initListConfig() {
4134
- this.listConfig = {
4135
- // The backend list is not paged yet; show the full set until it is.
4136
- paging: false,
4238
+ _initFilterConfig() {
4239
+ this.filterConfig = {
4240
+ items: [
4241
+ {
4242
+ name: 'keyword',
4243
+ type: ItemType.Keyword,
4244
+ label: 'Search',
4245
+ },
4246
+ {
4247
+ name: 'state',
4248
+ type: ItemType.Select,
4249
+ label: 'Status',
4250
+ multiple: true,
4251
+ values: () => ReportStates,
4252
+ isolate: { label: 'Show Deleted', value: ReportState.Deleted },
4253
+ },
4254
+ ],
4137
4255
  actions: [
4138
4256
  {
4139
- // Icon-only: the global Report Settings dialog.
4257
+ icon: 'unfold_more',
4258
+ tooltip: 'Expand all',
4259
+ primary: false,
4260
+ click: () => this._tree()?.expandAll(),
4261
+ },
4262
+ {
4263
+ icon: 'unfold_less',
4264
+ tooltip: 'Collapse all',
4265
+ primary: false,
4266
+ click: () => this._tree()?.collapseAll(),
4267
+ },
4268
+ {
4140
4269
  icon: 'settings',
4141
4270
  tooltip: 'Report settings',
4142
4271
  primary: false,
4143
4272
  click: () => this.openSettings(),
4144
4273
  },
4274
+ {
4275
+ label: 'New Folder',
4276
+ primary: false,
4277
+ click: () => this._createFolder(null),
4278
+ },
4145
4279
  {
4146
4280
  label: 'Create',
4147
4281
  primary: true,
4148
4282
  click: () => this.createReport(),
4149
4283
  },
4150
4284
  ],
4151
- rowActions: [
4285
+ // The two items are answered in different places and both have to run.
4286
+ // The keyword drives the tree's own search, which is the whole point of
4287
+ // searching a tree — it reveals a match nested inside collapsed folders
4288
+ // rather than only filtering what is already on screen. `state` is the
4289
+ // Show Deleted toggle, and only the server can answer that.
4290
+ change: (query) => {
4291
+ const stateChanged = String(query?.state ?? '') !== String(this._query.state ?? '');
4292
+ this._query = { ...query };
4293
+ this._search(query?.keyword);
4294
+ if (stateChanged) {
4295
+ this._load();
4296
+ }
4297
+ },
4298
+ };
4299
+ }
4300
+ _initTreeConfig() {
4301
+ this.treeConfig = {
4302
+ data: [],
4303
+ childrenName: 'children',
4304
+ expandLevel: FsAiReportsComponent.EXPAND_LEVEL,
4305
+ draggable: true,
4306
+ // The whole row is the grip and the handle stays hidden; a press only
4307
+ // becomes a drag once the pointer moves, so the kebab, the chevron and the
4308
+ // report link all stay clickable.
4309
+ dragMode: TreeDragMode.Node,
4310
+ // canDrop is asked about the node being DRAGGED, so only `toParent`
4311
+ // describes the destination — reading node.level/node.parent here would
4312
+ // describe where it came from. A report can never be a parent because its
4313
+ // type says so, and that single test is the whole "folders nest, reports
4314
+ // are leaves" rule on this side.
4315
+ canDrop: (node, fromParent, toParent) => {
4316
+ return !toParent || toParent.data?.type === ReportNodeType.Folder;
4317
+ },
4318
+ changeReorder: (event) => this._move(event),
4319
+ nodeClass: (node) => node.data?.type,
4320
+ // Match on the name only. The library's default matcher stringifies the
4321
+ // whole node, so it would hit ids, the literal type word, and everything
4322
+ // else riding on the row.
4323
+ filterItem: (node, query) => {
4324
+ return String(node.data?.name ?? '')
4325
+ .toLowerCase()
4326
+ .includes(String(query ?? '').toLowerCase());
4327
+ },
4328
+ actions: [
4152
4329
  {
4153
- click: (report) => this._reportData.delete(report.id),
4154
- remove: {
4155
- title: 'Confirm',
4156
- template: 'Are you sure you would like to delete this report?',
4157
- },
4158
- menu: true,
4159
- label: 'Delete',
4330
+ type: TreeActionType.Menu,
4331
+ icon: 'more_vert',
4332
+ items: [
4333
+ {
4334
+ label: 'Rename',
4335
+ show: (node) => !this._isDeletedNode(node),
4336
+ click: (node) => this._rename(node),
4337
+ },
4338
+ {
4339
+ label: 'New Folder Inside',
4340
+ show: (node) => this._isFolderNode(node),
4341
+ click: (node) => this._createFolder(node.data?.id),
4342
+ },
4343
+ {
4344
+ label: 'Delete',
4345
+ show: (node) => !this._isDeletedNode(node),
4346
+ click: (node) => this._delete(node),
4347
+ },
4348
+ // Folders never reach this: they hard delete, so a deleted folder
4349
+ // does not exist to be restored.
4350
+ {
4351
+ label: 'Restore',
4352
+ show: (node) => this._isDeletedNode(node),
4353
+ click: (node) => this._restore(node),
4354
+ },
4355
+ ],
4160
4356
  },
4161
4357
  ],
4162
- // `filter: true` makes fs-list render the Show Deleted toggle and send
4163
- // `query` with the fetch; in that mode it also swaps the row's Delete
4164
- // action for Restore.
4165
- restore: {
4166
- query: { state: ReportState.Deleted },
4167
- filter: true,
4168
- filterLabel: 'Show Deleted',
4169
- menuLabel: 'Restore',
4170
- reload: true,
4171
- click: (report) => this._reportData.undelete(report.id),
4172
- },
4173
- fetch: (query) => {
4174
- return this._reportData.reports(query)
4175
- .pipe(map((reports) => ({ data: reports ?? [] })));
4176
- },
4177
4358
  };
4178
4359
  }
4360
+ _isFolderNode(node) {
4361
+ return node?.data?.type === ReportNodeType.Folder;
4362
+ }
4363
+ _isDeletedNode(node) {
4364
+ return this.isDeleted(node?.data?.report);
4365
+ }
4366
+ // Drive the tree's built-in search: it collapses everything, hides every node,
4367
+ // then unhides each match and walks its ancestors open, so a report several
4368
+ // folders deep surfaces with its path expanded. Clearing does NOT undo that
4369
+ // initial collapse, so the tree is re-opened here — otherwise the listing is
4370
+ // left fully collapsed the moment somebody empties the search box.
4371
+ _search(keyword) {
4372
+ const tree = this._tree();
4373
+ if (!tree) {
4374
+ return;
4375
+ }
4376
+ if (keyword) {
4377
+ tree.search(keyword);
4378
+ return;
4379
+ }
4380
+ tree.clearSearch();
4381
+ tree.expandAll();
4382
+ }
4383
+ _load() {
4384
+ this._reportData.tree(this._query)
4385
+ .pipe(takeUntilDestroyed(this._destroyRef))
4386
+ .subscribe((tree) => {
4387
+ const nodes = this._buildNodes(tree);
4388
+ this.nodes.set(nodes);
4389
+ // The tree reads `config` once, in its own ngOnInit. The first load
4390
+ // lands before it exists, so the data has to arrive through the config;
4391
+ // every later one has to go through the instance or nothing changes.
4392
+ this.treeConfig = { ...this.treeConfig, data: nodes };
4393
+ this._tree()?.setData(nodes);
4394
+ });
4395
+ }
4396
+ // Flat API rows → the nested shape fs-tree renders. Folders come before
4397
+ // reports at every level, each already name-ordered by the backend.
4398
+ _buildNodes(tree) {
4399
+ const folders = tree?.folders ?? [];
4400
+ const reports = tree?.reports ?? [];
4401
+ const build = (parentId) => {
4402
+ const folderNodes = folders
4403
+ .filter((folder) => (folder.parentReportFolderId ?? null) === parentId)
4404
+ .map((folder) => ({
4405
+ key: `f:${folder.id}`,
4406
+ id: folder.id,
4407
+ name: folder.name,
4408
+ type: ReportNodeType.Folder,
4409
+ children: build(folder.id),
4410
+ }));
4411
+ const reportNodes = reports
4412
+ .filter((report) => (report.reportFolderId ?? null) === parentId)
4413
+ .map((report) => ({
4414
+ key: `r:${report.id}`,
4415
+ id: report.id,
4416
+ name: report.name,
4417
+ type: ReportNodeType.Report,
4418
+ children: [],
4419
+ report,
4420
+ }));
4421
+ return [...folderNodes, ...reportNodes];
4422
+ };
4423
+ return build(null);
4424
+ }
4425
+ // A drop moved a node under a new parent, or out to the root. The tree has
4426
+ // already moved it on screen, so both outcomes reload: a success to pick up
4427
+ // the server's ordering, a failure to put the row back where it really is.
4428
+ _move(event) {
4429
+ const node = event.node?.data;
4430
+ if (!node) {
4431
+ return;
4432
+ }
4433
+ const parentReportFolderId = event.toParent?.data?.id ?? null;
4434
+ if (event.toParent && !event.toParent.isExpanded()) {
4435
+ event.toParent.expand();
4436
+ }
4437
+ const save$ = node.type === ReportNodeType.Folder
4438
+ ? this._reportData.updateFolder(node.id, { parentReportFolderId })
4439
+ : this._reportData.moveReport(node.id, parentReportFolderId);
4440
+ save$
4441
+ .pipe(takeUntilDestroyed(this._destroyRef))
4442
+ .subscribe({
4443
+ next: () => {
4444
+ this._message.success('Saved changes');
4445
+ this._load();
4446
+ },
4447
+ error: () => this._load(),
4448
+ });
4449
+ }
4450
+ _createFolder(parentReportFolderId) {
4451
+ this._prompt.input({
4452
+ title: 'New Folder',
4453
+ label: 'Name',
4454
+ commitLabel: 'Create',
4455
+ required: true,
4456
+ })
4457
+ .pipe(switchMap$1((name) => this._reportData.createFolder(name, parentReportFolderId)), takeUntilDestroyed(this._destroyRef))
4458
+ .subscribe(() => {
4459
+ this._message.success('Created folder');
4460
+ this._load();
4461
+ });
4462
+ }
4463
+ _rename(node) {
4464
+ const data = node?.data;
4465
+ if (!data) {
4466
+ return;
4467
+ }
4468
+ const folder = data.type === ReportNodeType.Folder;
4469
+ this._prompt.input({
4470
+ title: folder ? 'Rename Folder' : 'Rename Report',
4471
+ label: 'Name',
4472
+ default: data.name,
4473
+ commitLabel: 'Save',
4474
+ required: true,
4475
+ })
4476
+ .pipe(switchMap$1((name) => (folder
4477
+ ? this._reportData.updateFolder(data.id, { name })
4478
+ : this._reportData.rename(data.id, name))), takeUntilDestroyed(this._destroyRef))
4479
+ .subscribe(() => {
4480
+ this._message.success('Saved changes');
4481
+ this._load();
4482
+ });
4483
+ }
4484
+ // Deleting a folder is refused server-side while it still holds a sub-folder
4485
+ // or an active report, and the message says which — so the confirm here warns
4486
+ // that a folder delete is permanent rather than trying to restate the rule.
4487
+ _delete(node) {
4488
+ const data = node?.data;
4489
+ if (!data) {
4490
+ return;
4491
+ }
4492
+ const folder = data.type === ReportNodeType.Folder;
4493
+ this._prompt.confirm({
4494
+ title: 'Confirm',
4495
+ template: folder
4496
+ ? `Are you sure you would like to delete the folder "${data.name}"? This cannot be undone.`
4497
+ : 'Are you sure you would like to delete this report?',
4498
+ })
4499
+ .pipe(switchMap$1(() => (folder
4500
+ ? this._reportData.deleteFolder(data.id)
4501
+ : this._reportData.delete(data.id))), takeUntilDestroyed(this._destroyRef))
4502
+ .subscribe(() => {
4503
+ this._message.success(folder ? 'Deleted folder' : 'Deleted report');
4504
+ this._load();
4505
+ });
4506
+ }
4507
+ _restore(node) {
4508
+ const data = node?.data;
4509
+ if (!data) {
4510
+ return;
4511
+ }
4512
+ this._reportData.undelete(data.id)
4513
+ .pipe(takeUntilDestroyed(this._destroyRef))
4514
+ .subscribe(() => {
4515
+ this._message.success('Restored report');
4516
+ this._load();
4517
+ });
4518
+ }
4519
+ // Turn a ':id' pattern into router commands. Relative unless it starts
4520
+ // with '/', in which case the first segment carries the leading slash.
4521
+ _patternCommands(pattern, id) {
4522
+ const absolute = pattern.startsWith('/');
4523
+ const segments = pattern.split('/')
4524
+ .filter((segment) => segment.length > 0)
4525
+ .map((segment) => (segment === ':id' ? id : segment));
4526
+ if (!segments.length) {
4527
+ return [id];
4528
+ }
4529
+ if (absolute) {
4530
+ segments[0] = `/${segments[0]}`;
4531
+ }
4532
+ return segments;
4533
+ }
4179
4534
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4180
- 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: "list", first: true, predicate: FsListComponent, descendants: true }], ngImport: i0, template: "<fs-list\n [config]=\"listConfig\"\n #list>\n\n <fs-list-column title=\"Name\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <!-- A deleted report can't be opened (the backend only assembles active\n ones), so in Show Deleted mode the name is plain text \u2014 restore it\n from the row menu first. -->\n @if (row.state === deletedState) {\n {{ row.name }}\n } @else {\n <a [routerLink]=\"openLink(row)\">{{ row.name }}</a>\n }\n </ng-template>\n </fs-list-column>\n\n <fs-list-column title=\"Modified\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <fs-date [date]=\"row.modifyDate\"></fs-date>\n </ng-template>\n </fs-list-column>\n\n</fs-list>\n", styles: ["a{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2$4.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsListModule }, { kind: "component", type: i1.FsListComponent, selector: "fs-list", inputs: ["config", "loaderLines", "cellRowType"], outputs: ["filtersReady"] }, { kind: "directive", type: i1.FsListColumnDirective, selector: "fs-list-column", inputs: ["show", "title", "name", "customizable", "sortable", "sortableDefault", "sortableDirection", "direction", "align", "width", "class"] }, { kind: "directive", type: i1.FsListCellDirective, selector: "[fs-list-cell],[fsListCell]", inputs: ["colspan", "align", "class", "fsListCell", "configTyping"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4535
+ 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$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i3$1.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i3$1.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$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 });
4181
4536
  }
4182
4537
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
4183
4538
  type: Component,
4184
4539
  args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
4185
- FsDateModule,
4186
- FsListModule,
4540
+ FsFilterModule,
4541
+ FsSkeletonModule,
4542
+ FsTreeModule,
4543
+ MatIconModule,
4187
4544
  RouterLink,
4188
- ], template: "<fs-list\n [config]=\"listConfig\"\n #list>\n\n <fs-list-column title=\"Name\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <!-- A deleted report can't be opened (the backend only assembles active\n ones), so in Show Deleted mode the name is plain text \u2014 restore it\n from the row menu first. -->\n @if (row.state === deletedState) {\n {{ row.name }}\n } @else {\n <a [routerLink]=\"openLink(row)\">{{ row.name }}</a>\n }\n </ng-template>\n </fs-list-column>\n\n <fs-list-column title=\"Modified\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <fs-date [date]=\"row.modifyDate\"></fs-date>\n </ng-template>\n </fs-list-column>\n\n</fs-list>\n", styles: ["a{cursor:pointer}\n"] }]
4189
- }], ctorParameters: () => [], propDecorators: { list: [{
4190
- type: ViewChild,
4191
- args: [FsListComponent]
4192
- }] } });
4545
+ ], 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"] }]
4546
+ }], ctorParameters: () => [] });
4193
4547
 
4194
4548
  // fs-filter query key for the runtime Frequency control (a render-layer bucket
4195
4549
  // override, not a filter group — so it's read straight off the query).
@@ -4215,6 +4569,9 @@ class FsAiReportComponent {
4215
4569
  reportFilterConfig;
4216
4570
  _split;
4217
4571
  _chatPanel;
4572
+ // Only the canvas knows which page is on screen, and a page-scoped export
4573
+ // has to ask it.
4574
+ _canvas;
4218
4575
  _reportData = inject(ReportData);
4219
4576
  _filterState = inject(ReportFilterStateService);
4220
4577
  _exportCollector = inject(ReportExportCollectorService);
@@ -4318,6 +4675,19 @@ class FsAiReportComponent {
4318
4675
  this._process.run('Exporting PDF', this._exportCollector.collect(report)
4319
4676
  .pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
4320
4677
  }
4678
+ // The raw data behind the whole report in one action: the server exports every
4679
+ // component's full filtered dataset and zips one CSV per component, foldered
4680
+ // by page. `thisPage` narrows it to the page the canvas is showing. Unlike the
4681
+ // PDF/PowerPoint exports this is entirely server-side — nothing is collected
4682
+ // in the browser, so it is not capped by what a slide can hold.
4683
+ exportData(thisPage = false) {
4684
+ const report = this.report;
4685
+ if (!report) {
4686
+ return;
4687
+ }
4688
+ const pageId = thisPage ? this._canvas?.activePage?.id ?? null : null;
4689
+ this._process.download('Exporting data', this._reportData.exportReport(report.id, this._filterState.resolveForReport(report, pageId), pageId));
4690
+ }
4321
4691
  // React to each chat turn (fs-chat handles the request + persistence); we
4322
4692
  // only care about the report side effects this context's backend flags.
4323
4693
  onChatResponse(response) {
@@ -4443,8 +4813,8 @@ class FsAiReportComponent {
4443
4813
  ReportPptxService,
4444
4814
  ReportPdfService,
4445
4815
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
4446
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
4447
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: 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 <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</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 </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]=\"{ reportId: reportId() }\"\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 (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\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__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$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$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"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4816
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
4817
+ ], 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 <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</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]=\"{ reportId: reportId() }\"\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 (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\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__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$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$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"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4448
4818
  }
4449
4819
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
4450
4820
  type: Component,
@@ -4456,7 +4826,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4456
4826
  ReportPptxService,
4457
4827
  ReportPdfService,
4458
4828
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
4459
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
4829
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
4460
4830
  ], imports: [
4461
4831
  FsFilterModule,
4462
4832
  FsMenuModule,
@@ -4465,13 +4835,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4465
4835
  MatIconModule,
4466
4836
  MatTooltipModule,
4467
4837
  ReportCanvasComponent,
4468
- ], 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 <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</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 </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]=\"{ reportId: reportId() }\"\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 (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\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__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"] }]
4838
+ ], 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 <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</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]=\"{ reportId: reportId() }\"\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 (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\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__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"] }]
4469
4839
  }], ctorParameters: () => [], propDecorators: { _split: [{
4470
4840
  type: ViewChild,
4471
4841
  args: ['split', { static: true }]
4472
4842
  }], _chatPanel: [{
4473
4843
  type: ViewChild,
4474
4844
  args: ['chatPanel', { static: true, read: ElementRef }]
4845
+ }], _canvas: [{
4846
+ type: ViewChild,
4847
+ args: [ReportCanvasComponent]
4475
4848
  }] } });
4476
4849
 
4477
4850
  /*
@@ -4486,5 +4859,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4486
4859
  * Generated bundle index. Do not edit.
4487
4860
  */
4488
4861
 
4489
- export { FsAiReportsComponent as F, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, FREQUENCY_OPTIONS as f, ReportState as g };
4490
- //# sourceMappingURL=firestitch-report-firestitch-report-_MCMM4XH.mjs.map
4862
+ export { FsAiReportsComponent as F, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, FREQUENCY_OPTIONS as f, ReportNodeType as g, ReportState as h, ReportStates as i };
4863
+ //# sourceMappingURL=firestitch-report-firestitch-report-D5cx4IJg.mjs.map