@firestitch/report 18.0.19 → 18.0.21

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.
Files changed (23) hide show
  1. package/app/reports/data/report.data.d.ts +8 -2
  2. package/app/reports/dialogs/report-settings/report-settings.component.d.ts +4 -2
  3. package/app/reports/interfaces/report.interface.d.ts +26 -2
  4. package/app/reports/layout.d.ts +2 -0
  5. package/app/reports/services/report-filter-state.service.d.ts +4 -1
  6. package/app/reports/views/report/fs-ai-report.component.d.ts +2 -0
  7. package/app/reports/views/report/report.component.d.ts +2 -0
  8. package/app/reports/views/reports/fs-ai-reports.component.d.ts +28 -8
  9. package/esm2022/app/reports/data/report.data.mjs +53 -4
  10. package/esm2022/app/reports/dialogs/report-settings/report-settings.component.mjs +16 -11
  11. package/esm2022/app/reports/interfaces/report.interface.mjs +12 -1
  12. package/esm2022/app/reports/layout.mjs +6 -1
  13. package/esm2022/app/reports/services/report-filter-state.service.mjs +55 -4
  14. package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +27 -5
  15. package/esm2022/app/reports/views/report/report.component.mjs +27 -5
  16. package/esm2022/app/reports/views/reports/fs-ai-reports.component.mjs +342 -74
  17. package/fesm2022/{firestitch-report-echarts-CfwgLaOA.mjs → firestitch-report-echarts-BYmAhi-N.mjs} +2 -2
  18. package/fesm2022/{firestitch-report-echarts-CfwgLaOA.mjs.map → firestitch-report-echarts-BYmAhi-N.mjs.map} +1 -1
  19. package/fesm2022/{firestitch-report-firestitch-report-_MCMM4XH.mjs → firestitch-report-firestitch-report-acZuB8V2.mjs} +529 -99
  20. package/fesm2022/firestitch-report-firestitch-report-acZuB8V2.mjs.map +1 -0
  21. package/fesm2022/firestitch-report.mjs +1 -1
  22. package/package.json +1 -1
  23. package/fesm2022/firestitch-report-firestitch-report-_MCMM4XH.mjs.map +0 -1
@@ -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,20 +82,58 @@ 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',
92
131
  ...config,
93
132
  });
94
133
  }
95
- // Report settings: any subset of { name, pageSize, pageOrientation, layout,
134
+ // Report settings: any subset of { name, pageWidth, pageHeight, layout,
96
135
  // styles } — styles carry the report's typographic config (e.g. heading size).
136
+ // The page size is in inches and width/height go together.
97
137
  update(reportId, settings, config = {}) {
98
138
  return this._api.put(this._path(`${reportId}`), settings, {
99
139
  key: 'report',
@@ -165,6 +205,17 @@ class ReportData {
165
205
  ...config,
166
206
  });
167
207
  }
208
+ // Exports EVERY component on the report — or on one page — server-side and
209
+ // resolves to a presigned URL for a zip of one CSV per component, foldered by
210
+ // page. `filters` carries the resolved values for every component together;
211
+ // each one picks out its own. Same keep-alive contract as exportComponent,
212
+ // over a longer run; consume through FsProcess.download.
213
+ exportReport(reportId, filters = [], pageId = null, config = {}) {
214
+ return this._api.post(this._path(`${reportId}/export`), { filters, ...(pageId ? { pageId } : {}) }, {
215
+ key: 'url',
216
+ ...config,
217
+ });
218
+ }
168
219
  // Distinct options for a select filter (drives multiselect controls).
169
220
  filterOptions(reportId, filterId, config = {}) {
170
221
  return this._api.get(this._path(`${reportId}/filters/${filterId}/options`), {}, {
@@ -338,6 +389,11 @@ function pageGeometry(report, page) {
338
389
  }
339
390
  return geometry;
340
391
  }
392
+ // Bounds for a page dimension, in inches. Mirrors ReportModel::PAGE_DIMENSION_MIN
393
+ // / MAX on the server, which is the enforcing side — these drive the field-level
394
+ // message so the operator sees the limit before submitting.
395
+ const PAGE_DIMENSION_MIN = 1;
396
+ const PAGE_DIMENSION_MAX = 200;
341
397
 
342
398
  // A date-range boundary as the picked LOCAL calendar day (no time, no tz).
343
399
  // Date-range filters are calendar-day semantics, so the boundary must carry
@@ -478,6 +534,36 @@ function selectQueryValues(raw) {
478
534
  return String(raw).split(',');
479
535
  }
480
536
 
537
+ // Whether two stored filter-group defaults are the same. Compared on the fields a
538
+ // default can actually carry, so key order in the payload can't read as a change.
539
+ function sameDefault(a, b) {
540
+ if (!a || !b) {
541
+ return !a && !b;
542
+ }
543
+ return a.start === b.start
544
+ && a.end === b.end
545
+ && a.value === b.value
546
+ && a.relative === b.relative
547
+ && (a.values ?? []).length === (b.values ?? []).length
548
+ && (a.values ?? []).every((value, index) => value === (b.values ?? [])[index]);
549
+ }
550
+ // A stable signature for a group's stored default. The report toolbar keys its
551
+ // fs-filter on this, because fs-filter reads its config once at init — without a
552
+ // key change it keeps rendering the values it was created with even after the
553
+ // config behind it has been rebuilt.
554
+ function defaultSignature(group) {
555
+ const groupDefault = group.config?.default;
556
+ if (!groupDefault) {
557
+ return '';
558
+ }
559
+ return [
560
+ groupDefault.start ?? '',
561
+ groupDefault.end ?? '',
562
+ groupDefault.value ?? '',
563
+ groupDefault.relative ?? '',
564
+ (groupDefault.values ?? []).join(','),
565
+ ].join('~');
566
+ }
481
567
  // Session filter state for the open report. Filter VALUES are never persisted
482
568
  // — this service holds them per filter GROUP (the propagation unit), resolves
483
569
  // them into per-component member-filter values for each data request, and
@@ -501,18 +587,28 @@ class ReportFilterStateService {
501
587
  _changed$ = new Subject();
502
588
  // Seed groups + their defaults when a report loads. Existing session values
503
589
  // for still-existing groups survive a structure re-fetch (a chat edit
504
- // shouldn't reset the viewer's filters).
590
+ // shouldn't reset the viewer's filters) — UNLESS the group's stored default
591
+ // itself changed, which only happens when an operator saved a new default in
592
+ // report settings. That is an explicit instruction to re-seed, so it wins over
593
+ // the surviving session value; without it the bar keeps showing whatever it was
594
+ // first seeded with until a full page reload.
505
595
  init(report) {
506
- this._groups.clear();
596
+ const previousGroups = this._groups;
597
+ this._groups = new Map();
507
598
  const validIds = new Set();
508
599
  for (const group of report.filterGroups ?? []) {
509
600
  this._groups.set(group.id, group);
510
601
  validIds.add(group.id);
511
- if (!this._values.has(group.id)) {
602
+ const defaultChanged = !sameDefault(previousGroups.get(group.id)?.config?.default ?? null, group.config?.default ?? null);
603
+ if (!this._values.has(group.id) || defaultChanged) {
512
604
  const defaultValue = this._defaultValue(group);
513
605
  if (defaultValue) {
514
606
  this._values.set(group.id, defaultValue);
515
607
  }
608
+ else if (defaultChanged) {
609
+ // The default was cleared, so the value it seeded goes with it.
610
+ this._values.delete(group.id);
611
+ }
516
612
  }
517
613
  }
518
614
  for (const groupId of [...this._values.keys()]) {
@@ -599,6 +695,17 @@ class ReportFilterStateService {
599
695
  }
600
696
  return resolved;
601
697
  }
698
+ // Every component's resolved values in one array — what a report-level export
699
+ // carries so each file in the archive is filtered exactly as the screen is.
700
+ // Filter ids are unique per component, so the backend picks each component's
701
+ // own out of the pile. A page id narrows it to that page.
702
+ resolveForReport(report, pageId = null) {
703
+ const pages = (report.pages ?? [])
704
+ .filter((page) => !pageId || page.id === pageId);
705
+ return pages
706
+ .flatMap((page) => (page.components ?? [])
707
+ .flatMap((component) => this.resolveForComponent(component)));
708
+ }
602
709
  // A group's authored default, mapped to a session value by type. Relative
603
710
  // date presets resolve against "now" each time the report opens, so a saved
604
711
  // "last 12 months" stays relative; a fixed range, select values and keyword
@@ -2317,7 +2424,7 @@ function dateToBound(value) {
2317
2424
  }
2318
2425
 
2319
2426
  // Report settings, in three tabs:
2320
- // Settings — name + page setup (size, orientation, layout mode).
2427
+ // Settings — name + page setup (size in inches, layout mode).
2321
2428
  // Styles — the report's typographic styles (sizes in POINTS, 1:1 with
2322
2429
  // PDF/PowerPoint); the Heading size to start.
2323
2430
  // Filters — the report-level filters and their DEFAULT values. A default
@@ -2328,8 +2435,13 @@ function dateToBound(value) {
2328
2435
  class ReportSettingsComponent {
2329
2436
  selectedTab = 'settings';
2330
2437
  name = '';
2331
- pageSize = 'widescreen';
2332
- pageOrientation = 'landscape';
2438
+ // The page in inches. No initializer: every report carries a size, and
2439
+ // ngOnInit seeds both before the tab renders, so a placeholder here could only
2440
+ // ever be a wrong number briefly showing as the operator's own.
2441
+ pageWidth;
2442
+ pageHeight;
2443
+ pageDimensionMin = PAGE_DIMENSION_MIN;
2444
+ pageDimensionMax = PAGE_DIMENSION_MAX;
2333
2445
  layout = 'freeform';
2334
2446
  // Empty until the report has one; app-timezone-select then defaults it to the
2335
2447
  // viewer's browser zone (which Save persists, making the report's zone explicit).
@@ -2346,8 +2458,8 @@ class ReportSettingsComponent {
2346
2458
  ngOnInit() {
2347
2459
  const report = this._data.report;
2348
2460
  this.name = report.name;
2349
- this.pageSize = report.pageSize;
2350
- this.pageOrientation = report.pageOrientation;
2461
+ this.pageWidth = report.pageWidth;
2462
+ this.pageHeight = report.pageHeight;
2351
2463
  this.layout = report.layout ?? 'freeform';
2352
2464
  this.timezone = report.timezone ?? '';
2353
2465
  this.headingSize = report.config?.styles?.heading?.size ?? DEFAULT_HEADING_SIZE;
@@ -2374,8 +2486,8 @@ class ReportSettingsComponent {
2374
2486
  // result, re-seeding the bar from the new defaults.
2375
2487
  const settings$ = this._reportData.update(reportId, {
2376
2488
  name: this.name,
2377
- pageSize: this.pageSize,
2378
- pageOrientation: this.pageOrientation,
2489
+ pageWidth: this.pageWidth,
2490
+ pageHeight: this.pageHeight,
2379
2491
  layout: this.layout,
2380
2492
  timezone: this.timezone,
2381
2493
  styles: { heading: { size: this.headingSize } },
@@ -2485,7 +2597,7 @@ class ReportSettingsComponent {
2485
2597
  return Object.keys(normalized).length ? normalized : null;
2486
2598
  }
2487
2599
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2488
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report-level filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i2$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i2$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i6.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { 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: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption$1, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2600
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <span matTextSuffix>\n inches\n </span>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <span matTextSuffix>\n inches\n </span>\n </mat-form-field>\n </div>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report-level filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i2$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i2$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i6.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { 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: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption$1, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2489
2601
  }
2490
2602
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, decorators: [{
2491
2603
  type: Component,
@@ -2510,7 +2622,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2510
2622
  MatSelect,
2511
2623
  MatOption$1,
2512
2624
  TimezoneSelectComponent,
2513
- ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report-level filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"] }]
2625
+ ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <span matTextSuffix>\n inches\n </span>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <span matTextSuffix>\n inches\n </span>\n </mat-form-field>\n </div>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report-level filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"] }]
2514
2626
  }] });
2515
2627
 
2516
2628
  // The assembled report structure returned by GET /api/reports/:id — composed
@@ -2528,13 +2640,24 @@ const FREQUENCY_OPTIONS = [
2528
2640
  { value: 'quarter', name: 'Quarter' },
2529
2641
  { value: 'year', name: 'Annually' },
2530
2642
  ];
2643
+ var ReportNodeType;
2644
+ (function (ReportNodeType) {
2645
+ ReportNodeType["Folder"] = "folder";
2646
+ ReportNodeType["Report"] = "report";
2647
+ })(ReportNodeType || (ReportNodeType = {}));
2531
2648
  // Reports are soft-deletable: a deleted report leaves the listing but keeps its
2532
2649
  // pages and components, and the listing's "Show Deleted" mode restores it.
2650
+ // Folders have no equivalent — they are only deletable once empty, so they hard
2651
+ // delete and there is nothing to restore.
2533
2652
  var ReportState;
2534
2653
  (function (ReportState) {
2535
2654
  ReportState["Active"] = "active";
2536
2655
  ReportState["Deleted"] = "deleted";
2537
2656
  })(ReportState || (ReportState = {}));
2657
+ const ReportStates = [
2658
+ { name: 'Active', value: ReportState.Active },
2659
+ { name: 'Deleted', value: ReportState.Deleted },
2660
+ ];
2538
2661
 
2539
2662
  // Rows fetched for a list component's export table — enough to fill a
2540
2663
  // slide/page table; the cap note covers the rest.
@@ -2648,7 +2771,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
2648
2771
  // complete synchronously after setOption.
2649
2772
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
2650
2773
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
2651
- const echarts = (await import('./firestitch-report-echarts-CfwgLaOA.mjs')).default;
2774
+ const echarts = (await import('./firestitch-report-echarts-BYmAhi-N.mjs')).default;
2652
2775
  const host = document.createElement('div');
2653
2776
  host.style.width = `${width}px`;
2654
2777
  host.style.height = `${height}px`;
@@ -3122,6 +3245,9 @@ class ReportComponent {
3122
3245
  reportFilterConfig;
3123
3246
  _split;
3124
3247
  _chatPanel;
3248
+ // Only the canvas knows which page is on screen, and a page-scoped export
3249
+ // has to ask it.
3250
+ _canvas;
3125
3251
  introMessage = {
3126
3252
  text: '',
3127
3253
  };
@@ -3254,6 +3380,19 @@ class ReportComponent {
3254
3380
  this._process.run('Exporting PDF', this._exportCollector.collect(report)
3255
3381
  .pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
3256
3382
  }
3383
+ // The raw data behind the whole report in one action: the server exports every
3384
+ // component's full filtered dataset and zips one CSV per component, foldered
3385
+ // by page. `thisPage` narrows it to the page the canvas is showing. Unlike the
3386
+ // PDF/PowerPoint exports this is entirely server-side — nothing is collected
3387
+ // in the browser, so it is not capped by what a slide can hold.
3388
+ exportData(thisPage = false) {
3389
+ const report = this.report;
3390
+ if (!report) {
3391
+ return;
3392
+ }
3393
+ const pageId = thisPage ? this._canvas?.activePage?.id ?? null : null;
3394
+ this._process.download('Exporting data', this._reportData.exportReport(report.id, this._filterState.resolveForReport(report, pageId), pageId));
3395
+ }
3257
3396
  // React to each chat turn (fs-chat handles the request + persistence); we
3258
3397
  // only care about the report side effects this context's backend flags.
3259
3398
  onChatResponse(response) {
@@ -3327,7 +3466,10 @@ class ReportComponent {
3327
3466
  // changes, e.g. after the component settings dialog adds/moves a filter.
3328
3467
  get reportFilterKey() {
3329
3468
  return [
3330
- ...this._reportGroups().map((group) => `${group.id}:${group.level}:${group.label}`),
3469
+ // The stored default is part of the signature: saving a new default in report
3470
+ // settings rebuilds the config, but without a key change fs-filter keeps
3471
+ // rendering the values it was created with until a full page reload.
3472
+ ...this._reportGroups().map((group) => `${group.id}:${group.level}:${group.label}:${defaultSignature(group)}`),
3331
3473
  this._hasTimeSeriesChart() ? 'frequency' : '',
3332
3474
  ].join('|');
3333
3475
  }
@@ -3394,8 +3536,8 @@ class ReportComponent {
3394
3536
  ReportPdfService,
3395
3537
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3396
3538
  // 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 });
3539
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-BYmAhi-N.mjs').then((module) => module.default) }),
3540
+ ], 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
3541
  }
3400
3542
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
3401
3543
  type: Component,
@@ -3408,7 +3550,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3408
3550
  ReportPdfService,
3409
3551
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
3410
3552
  // this route's chunk.
3411
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
3553
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-BYmAhi-N.mjs').then((module) => module.default) }),
3412
3554
  ], imports: [
3413
3555
  FormsModule,
3414
3556
  FsAutocompleteChipsModule,
@@ -3417,13 +3559,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3417
3559
  FsAiChatComponent,
3418
3560
  MatIcon,
3419
3561
  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"] }]
3562
+ ], 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
3563
  }], ctorParameters: () => [], propDecorators: { _split: [{
3422
3564
  type: ViewChild,
3423
3565
  args: ['split', { static: true }]
3424
3566
  }], _chatPanel: [{
3425
3567
  type: ViewChild,
3426
3568
  args: ['chatPanel', { static: true, read: ElementRef }]
3569
+ }], _canvas: [{
3570
+ type: ViewChild,
3571
+ args: [ReportCanvasComponent]
3427
3572
  }] } });
3428
3573
 
3429
3574
  // The AI Knowledge document's version history (append-only), each version
@@ -4050,18 +4195,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4050
4195
  ], 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
4196
  }], ctorParameters: () => [] });
4052
4197
 
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.
4198
+ // The reports listing — an fs-tree of folders and the reports filed into them.
4199
+ // Rows link out through a URL pattern (a ':id' token → the report id; a leading
4200
+ // '/' makes it absolute, otherwise it's relative to this component's route).
4201
+ // Create prompts for a title, creates the report, then routes to it. The gear
4202
+ // opens the global Report Settings dialog (AI knowledge document +
4203
+ // semantic-database build), which lives in this package. No routing is
4204
+ // hard-coded here.
4205
+ //
4206
+ // Folders nest; reports are always leaves — canDrop enforces it here, and the
4207
+ // backend enforces it structurally by keeping the tree in its own table. A
4208
+ // report in no folder is a root node, which is where every report sat before
4209
+ // folders existed, so nothing had to be migrated or bucketed as "uncategorized".
4059
4210
  //
4060
4211
  // 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.
4212
+ // listing into deleted mode (the backend hides deleted reports otherwise), and
4213
+ // in that mode the kebab offers Restore instead. Folders do NOT take part in
4214
+ // that — a folder is only deletable once it is empty, so it hard deletes and has
4215
+ // nothing to restore. The upshot is that deleted mode keeps the folder structure
4216
+ // on screen with the deleted reports sitting where they belong, so restoring one
4217
+ // puts it back rather than dropping it at the root.
4064
4218
  class FsAiReportsComponent {
4219
+ // How deep the tree opens on load. Folders are audience buckets, not a
4220
+ // filesystem, so there is nothing worth hiding behind a closed chevron.
4221
+ static EXPAND_LEVEL = 10;
4065
4222
  // API base for the reports endpoints; override to mount elsewhere.
4066
4223
  basePath = input('reports');
4067
4224
  // Where a row links to; ':id' is replaced with the report id. Default takes
@@ -4069,27 +4226,44 @@ class FsAiReportsComponent {
4069
4226
  openPattern = input(':id');
4070
4227
  // Where a freshly-created report routes to; same ':id' pattern rules.
4071
4228
  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;
4229
+ // Null until the first load resolves — that is what gates *fsSkeleton.
4230
+ nodes = signal(null);
4231
+ treeConfig;
4232
+ filterConfig;
4233
+ // Template-side handle on the enum, so a row can tell a folder from a report.
4234
+ nodeType = ReportNodeType;
4235
+ _tree = viewChild(FsTreeComponent);
4077
4236
  _reportData = inject(ReportData);
4078
4237
  _router = inject(Router);
4079
4238
  _route = inject(ActivatedRoute);
4080
4239
  _prompt = inject(FsPrompt);
4240
+ _message = inject(FsMessage);
4081
4241
  _dialog = inject(MatDialog);
4082
4242
  _destroyRef = inject(DestroyRef);
4243
+ // The filter bar's current query — carries `state` when Show Deleted is on.
4244
+ _query = {};
4083
4245
  constructor() {
4084
4246
  effect(() => {
4085
4247
  this._reportData.basePath = this.basePath();
4086
4248
  });
4087
- this._initListConfig();
4249
+ }
4250
+ ngOnInit() {
4251
+ // Set before the first fetch rather than waiting on the effect above, which
4252
+ // does not run until after the initial change detection pass.
4253
+ this._reportData.basePath = this.basePath();
4254
+ this._initFilterConfig();
4255
+ this._initTreeConfig();
4256
+ this._load();
4088
4257
  }
4089
4258
  // routerLink commands for a report row (built from openPattern).
4090
4259
  openLink(report) {
4091
4260
  return this._patternCommands(this.openPattern(), report.id);
4092
4261
  }
4262
+ // A deleted report can't be opened — the backend only assembles active ones —
4263
+ // so in Show Deleted mode its name renders as plain text rather than a link.
4264
+ isDeleted(report) {
4265
+ return report?.state === ReportState.Deleted;
4266
+ }
4093
4267
  // Prompt for a title, create the report, then route to it (createPattern).
4094
4268
  createReport() {
4095
4269
  this._prompt.input({
@@ -4098,27 +4272,12 @@ class FsAiReportsComponent {
4098
4272
  commitLabel: 'Create',
4099
4273
  required: true,
4100
4274
  })
4101
- .pipe(switchMap((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
4275
+ .pipe(switchMap$1((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
4102
4276
  .subscribe((report) => {
4103
4277
  const pattern = this.createPattern();
4104
4278
  this._router.navigate(this._patternCommands(pattern, report.id), pattern.startsWith('/') ? {} : { relativeTo: this._route });
4105
4279
  });
4106
4280
  }
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
4281
  // The global Report Settings dialog: the AI knowledge document and the
4123
4282
  // semantic-database build. A running build streams into the FsProcess dock,
4124
4283
  // which survives the dialog closing.
@@ -4130,66 +4289,315 @@ class FsAiReportsComponent {
4130
4289
  autoFocus: false,
4131
4290
  });
4132
4291
  }
4133
- _initListConfig() {
4134
- this.listConfig = {
4135
- // The backend list is not paged yet; show the full set until it is.
4136
- paging: false,
4292
+ _initFilterConfig() {
4293
+ this.filterConfig = {
4294
+ items: [
4295
+ {
4296
+ name: 'keyword',
4297
+ type: ItemType.Keyword,
4298
+ label: 'Search',
4299
+ },
4300
+ {
4301
+ name: 'state',
4302
+ type: ItemType.Select,
4303
+ label: 'Status',
4304
+ multiple: true,
4305
+ values: () => ReportStates,
4306
+ isolate: { label: 'Show Deleted', value: ReportState.Deleted },
4307
+ },
4308
+ ],
4137
4309
  actions: [
4138
4310
  {
4139
- // Icon-only: the global Report Settings dialog.
4311
+ icon: 'unfold_more',
4312
+ tooltip: 'Expand all',
4313
+ primary: false,
4314
+ click: () => this._tree()?.expandAll(),
4315
+ },
4316
+ {
4317
+ icon: 'unfold_less',
4318
+ tooltip: 'Collapse all',
4319
+ primary: false,
4320
+ click: () => this._tree()?.collapseAll(),
4321
+ },
4322
+ {
4140
4323
  icon: 'settings',
4141
4324
  tooltip: 'Report settings',
4142
4325
  primary: false,
4143
4326
  click: () => this.openSettings(),
4144
4327
  },
4328
+ {
4329
+ label: 'New Folder',
4330
+ primary: false,
4331
+ click: () => this._createFolder(null),
4332
+ },
4145
4333
  {
4146
4334
  label: 'Create',
4147
4335
  primary: true,
4148
4336
  click: () => this.createReport(),
4149
4337
  },
4150
4338
  ],
4151
- rowActions: [
4339
+ // The two items are answered in different places and both have to run.
4340
+ // The keyword drives the tree's own search, which is the whole point of
4341
+ // searching a tree — it reveals a match nested inside collapsed folders
4342
+ // rather than only filtering what is already on screen. `state` is the
4343
+ // Show Deleted toggle, and only the server can answer that.
4344
+ change: (query) => {
4345
+ const stateChanged = String(query?.state ?? '') !== String(this._query.state ?? '');
4346
+ this._query = { ...query };
4347
+ this._search(query?.keyword);
4348
+ if (stateChanged) {
4349
+ this._load();
4350
+ }
4351
+ },
4352
+ };
4353
+ }
4354
+ _initTreeConfig() {
4355
+ this.treeConfig = {
4356
+ data: [],
4357
+ childrenName: 'children',
4358
+ expandLevel: FsAiReportsComponent.EXPAND_LEVEL,
4359
+ draggable: true,
4360
+ // The whole row is the grip and the handle stays hidden; a press only
4361
+ // becomes a drag once the pointer moves, so the kebab, the chevron and the
4362
+ // report link all stay clickable.
4363
+ dragMode: TreeDragMode.Node,
4364
+ // canDrop is asked about the node being DRAGGED, so only `toParent`
4365
+ // describes the destination — reading node.level/node.parent here would
4366
+ // describe where it came from. A report can never be a parent because its
4367
+ // type says so, and that single test is the whole "folders nest, reports
4368
+ // are leaves" rule on this side.
4369
+ canDrop: (node, fromParent, toParent) => {
4370
+ return !toParent || toParent.data?.type === ReportNodeType.Folder;
4371
+ },
4372
+ changeReorder: (event) => this._move(event),
4373
+ nodeClass: (node) => node.data?.type,
4374
+ // Match on the name only. The library's default matcher stringifies the
4375
+ // whole node, so it would hit ids, the literal type word, and everything
4376
+ // else riding on the row.
4377
+ filterItem: (node, query) => {
4378
+ return String(node.data?.name ?? '')
4379
+ .toLowerCase()
4380
+ .includes(String(query ?? '').toLowerCase());
4381
+ },
4382
+ actions: [
4152
4383
  {
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',
4384
+ type: TreeActionType.Menu,
4385
+ icon: 'more_vert',
4386
+ items: [
4387
+ {
4388
+ label: 'Rename',
4389
+ show: (node) => !this._isDeletedNode(node),
4390
+ click: (node) => this._rename(node),
4391
+ },
4392
+ {
4393
+ label: 'New Folder Inside',
4394
+ show: (node) => this._isFolderNode(node),
4395
+ click: (node) => this._createFolder(node.data?.id),
4396
+ },
4397
+ {
4398
+ label: 'Delete',
4399
+ show: (node) => !this._isDeletedNode(node),
4400
+ click: (node) => this._delete(node),
4401
+ },
4402
+ // Folders never reach this: they hard delete, so a deleted folder
4403
+ // does not exist to be restored.
4404
+ {
4405
+ label: 'Restore',
4406
+ show: (node) => this._isDeletedNode(node),
4407
+ click: (node) => this._restore(node),
4408
+ },
4409
+ ],
4160
4410
  },
4161
4411
  ],
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
4412
  };
4178
4413
  }
4414
+ _isFolderNode(node) {
4415
+ return node?.data?.type === ReportNodeType.Folder;
4416
+ }
4417
+ _isDeletedNode(node) {
4418
+ return this.isDeleted(node?.data?.report);
4419
+ }
4420
+ // Drive the tree's built-in search: it collapses everything, hides every node,
4421
+ // then unhides each match and walks its ancestors open, so a report several
4422
+ // folders deep surfaces with its path expanded. Clearing does NOT undo that
4423
+ // initial collapse, so the tree is re-opened here — otherwise the listing is
4424
+ // left fully collapsed the moment somebody empties the search box.
4425
+ _search(keyword) {
4426
+ const tree = this._tree();
4427
+ if (!tree) {
4428
+ return;
4429
+ }
4430
+ if (keyword) {
4431
+ tree.search(keyword);
4432
+ return;
4433
+ }
4434
+ tree.clearSearch();
4435
+ tree.expandAll();
4436
+ }
4437
+ _load() {
4438
+ this._reportData.tree(this._query)
4439
+ .pipe(takeUntilDestroyed(this._destroyRef))
4440
+ .subscribe((tree) => {
4441
+ const nodes = this._buildNodes(tree);
4442
+ this.nodes.set(nodes);
4443
+ // The tree reads `config` once, in its own ngOnInit. The first load
4444
+ // lands before it exists, so the data has to arrive through the config;
4445
+ // every later one has to go through the instance or nothing changes.
4446
+ this.treeConfig = { ...this.treeConfig, data: nodes };
4447
+ this._tree()?.setData(nodes);
4448
+ });
4449
+ }
4450
+ // Flat API rows → the nested shape fs-tree renders. Folders come before
4451
+ // reports at every level, each already name-ordered by the backend.
4452
+ _buildNodes(tree) {
4453
+ const folders = tree?.folders ?? [];
4454
+ const reports = tree?.reports ?? [];
4455
+ const build = (parentId) => {
4456
+ const folderNodes = folders
4457
+ .filter((folder) => (folder.parentReportFolderId ?? null) === parentId)
4458
+ .map((folder) => ({
4459
+ key: `f:${folder.id}`,
4460
+ id: folder.id,
4461
+ name: folder.name,
4462
+ type: ReportNodeType.Folder,
4463
+ children: build(folder.id),
4464
+ }));
4465
+ const reportNodes = reports
4466
+ .filter((report) => (report.reportFolderId ?? null) === parentId)
4467
+ .map((report) => ({
4468
+ key: `r:${report.id}`,
4469
+ id: report.id,
4470
+ name: report.name,
4471
+ type: ReportNodeType.Report,
4472
+ children: [],
4473
+ report,
4474
+ }));
4475
+ return [...folderNodes, ...reportNodes];
4476
+ };
4477
+ return build(null);
4478
+ }
4479
+ // A drop moved a node under a new parent, or out to the root. The tree has
4480
+ // already moved it on screen, so both outcomes reload: a success to pick up
4481
+ // the server's ordering, a failure to put the row back where it really is.
4482
+ _move(event) {
4483
+ const node = event.node?.data;
4484
+ if (!node) {
4485
+ return;
4486
+ }
4487
+ const parentReportFolderId = event.toParent?.data?.id ?? null;
4488
+ if (event.toParent && !event.toParent.isExpanded()) {
4489
+ event.toParent.expand();
4490
+ }
4491
+ const save$ = node.type === ReportNodeType.Folder
4492
+ ? this._reportData.updateFolder(node.id, { parentReportFolderId })
4493
+ : this._reportData.moveReport(node.id, parentReportFolderId);
4494
+ save$
4495
+ .pipe(takeUntilDestroyed(this._destroyRef))
4496
+ .subscribe({
4497
+ next: () => {
4498
+ this._message.success('Saved changes');
4499
+ this._load();
4500
+ },
4501
+ error: () => this._load(),
4502
+ });
4503
+ }
4504
+ _createFolder(parentReportFolderId) {
4505
+ this._prompt.input({
4506
+ title: 'New Folder',
4507
+ label: 'Name',
4508
+ commitLabel: 'Create',
4509
+ required: true,
4510
+ })
4511
+ .pipe(switchMap$1((name) => this._reportData.createFolder(name, parentReportFolderId)), takeUntilDestroyed(this._destroyRef))
4512
+ .subscribe(() => {
4513
+ this._message.success('Created folder');
4514
+ this._load();
4515
+ });
4516
+ }
4517
+ _rename(node) {
4518
+ const data = node?.data;
4519
+ if (!data) {
4520
+ return;
4521
+ }
4522
+ const folder = data.type === ReportNodeType.Folder;
4523
+ this._prompt.input({
4524
+ title: folder ? 'Rename Folder' : 'Rename Report',
4525
+ label: 'Name',
4526
+ default: data.name,
4527
+ commitLabel: 'Save',
4528
+ required: true,
4529
+ })
4530
+ .pipe(switchMap$1((name) => (folder
4531
+ ? this._reportData.updateFolder(data.id, { name })
4532
+ : this._reportData.rename(data.id, name))), takeUntilDestroyed(this._destroyRef))
4533
+ .subscribe(() => {
4534
+ this._message.success('Saved changes');
4535
+ this._load();
4536
+ });
4537
+ }
4538
+ // Deleting a folder is refused server-side while it still holds a sub-folder
4539
+ // or an active report, and the message says which — so the confirm here warns
4540
+ // that a folder delete is permanent rather than trying to restate the rule.
4541
+ _delete(node) {
4542
+ const data = node?.data;
4543
+ if (!data) {
4544
+ return;
4545
+ }
4546
+ const folder = data.type === ReportNodeType.Folder;
4547
+ this._prompt.confirm({
4548
+ title: 'Confirm',
4549
+ template: folder
4550
+ ? `Are you sure you would like to delete the folder "${data.name}"? This cannot be undone.`
4551
+ : 'Are you sure you would like to delete this report?',
4552
+ })
4553
+ .pipe(switchMap$1(() => (folder
4554
+ ? this._reportData.deleteFolder(data.id)
4555
+ : this._reportData.delete(data.id))), takeUntilDestroyed(this._destroyRef))
4556
+ .subscribe(() => {
4557
+ this._message.success(folder ? 'Deleted folder' : 'Deleted report');
4558
+ this._load();
4559
+ });
4560
+ }
4561
+ _restore(node) {
4562
+ const data = node?.data;
4563
+ if (!data) {
4564
+ return;
4565
+ }
4566
+ this._reportData.undelete(data.id)
4567
+ .pipe(takeUntilDestroyed(this._destroyRef))
4568
+ .subscribe(() => {
4569
+ this._message.success('Restored report');
4570
+ this._load();
4571
+ });
4572
+ }
4573
+ // Turn a ':id' pattern into router commands. Relative unless it starts
4574
+ // with '/', in which case the first segment carries the leading slash.
4575
+ _patternCommands(pattern, id) {
4576
+ const absolute = pattern.startsWith('/');
4577
+ const segments = pattern.split('/')
4578
+ .filter((segment) => segment.length > 0)
4579
+ .map((segment) => (segment === ':id' ? id : segment));
4580
+ if (!segments.length) {
4581
+ return [id];
4582
+ }
4583
+ if (absolute) {
4584
+ segments[0] = `/${segments[0]}`;
4585
+ }
4586
+ return segments;
4587
+ }
4179
4588
  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 });
4589
+ 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
4590
  }
4182
4591
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
4183
4592
  type: Component,
4184
4593
  args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
4185
- FsDateModule,
4186
- FsListModule,
4594
+ FsFilterModule,
4595
+ FsSkeletonModule,
4596
+ FsTreeModule,
4597
+ MatIconModule,
4187
4598
  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
- }] } });
4599
+ ], 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"] }]
4600
+ }], ctorParameters: () => [] });
4193
4601
 
4194
4602
  // fs-filter query key for the runtime Frequency control (a render-layer bucket
4195
4603
  // override, not a filter group — so it's read straight off the query).
@@ -4215,6 +4623,9 @@ class FsAiReportComponent {
4215
4623
  reportFilterConfig;
4216
4624
  _split;
4217
4625
  _chatPanel;
4626
+ // Only the canvas knows which page is on screen, and a page-scoped export
4627
+ // has to ask it.
4628
+ _canvas;
4218
4629
  _reportData = inject(ReportData);
4219
4630
  _filterState = inject(ReportFilterStateService);
4220
4631
  _exportCollector = inject(ReportExportCollectorService);
@@ -4318,6 +4729,19 @@ class FsAiReportComponent {
4318
4729
  this._process.run('Exporting PDF', this._exportCollector.collect(report)
4319
4730
  .pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
4320
4731
  }
4732
+ // The raw data behind the whole report in one action: the server exports every
4733
+ // component's full filtered dataset and zips one CSV per component, foldered
4734
+ // by page. `thisPage` narrows it to the page the canvas is showing. Unlike the
4735
+ // PDF/PowerPoint exports this is entirely server-side — nothing is collected
4736
+ // in the browser, so it is not capped by what a slide can hold.
4737
+ exportData(thisPage = false) {
4738
+ const report = this.report;
4739
+ if (!report) {
4740
+ return;
4741
+ }
4742
+ const pageId = thisPage ? this._canvas?.activePage?.id ?? null : null;
4743
+ this._process.download('Exporting data', this._reportData.exportReport(report.id, this._filterState.resolveForReport(report, pageId), pageId));
4744
+ }
4321
4745
  // React to each chat turn (fs-chat handles the request + persistence); we
4322
4746
  // only care about the report side effects this context's backend flags.
4323
4747
  onChatResponse(response) {
@@ -4393,7 +4817,10 @@ class FsAiReportComponent {
4393
4817
  // rebuilt config) whenever the report-level filter set changes.
4394
4818
  get reportFilterKey() {
4395
4819
  return [
4396
- ...this._reportGroups().map((group) => `${group.id}:${group.level}:${group.label}`),
4820
+ // The stored default is part of the signature: saving a new default in report
4821
+ // settings rebuilds the config, but without a key change fs-filter keeps
4822
+ // rendering the values it was created with until a full page reload.
4823
+ ...this._reportGroups().map((group) => `${group.id}:${group.level}:${group.label}:${defaultSignature(group)}`),
4397
4824
  this._hasTimeSeriesChart() ? 'frequency' : '',
4398
4825
  ].join('|');
4399
4826
  }
@@ -4443,8 +4870,8 @@ class FsAiReportComponent {
4443
4870
  ReportPptxService,
4444
4871
  ReportPdfService,
4445
4872
  // 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 });
4873
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-BYmAhi-N.mjs').then((module) => module.default) }),
4874
+ ], 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
4875
  }
4449
4876
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
4450
4877
  type: Component,
@@ -4456,7 +4883,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4456
4883
  ReportPptxService,
4457
4884
  ReportPdfService,
4458
4885
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
4459
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CfwgLaOA.mjs').then((module) => module.default) }),
4886
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-BYmAhi-N.mjs').then((module) => module.default) }),
4460
4887
  ], imports: [
4461
4888
  FsFilterModule,
4462
4889
  FsMenuModule,
@@ -4465,13 +4892,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4465
4892
  MatIconModule,
4466
4893
  MatTooltipModule,
4467
4894
  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"] }]
4895
+ ], 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
4896
  }], ctorParameters: () => [], propDecorators: { _split: [{
4470
4897
  type: ViewChild,
4471
4898
  args: ['split', { static: true }]
4472
4899
  }], _chatPanel: [{
4473
4900
  type: ViewChild,
4474
4901
  args: ['chatPanel', { static: true, read: ElementRef }]
4902
+ }], _canvas: [{
4903
+ type: ViewChild,
4904
+ args: [ReportCanvasComponent]
4475
4905
  }] } });
4476
4906
 
4477
4907
  /*
@@ -4486,5 +4916,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4486
4916
  * Generated bundle index. Do not edit.
4487
4917
  */
4488
4918
 
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
4919
+ 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 };
4920
+ //# sourceMappingURL=firestitch-report-firestitch-report-acZuB8V2.mjs.map