@firestitch/report 18.3.0 → 18.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,10 @@ import * as i0 from '@angular/core';
2
2
  import { inject, Injectable, Component, ChangeDetectionStrategy, Input, signal, DestroyRef, ViewChild, input, output, computed, viewChild, ElementRef, effect, untracked, EventEmitter, ChangeDetectorRef, NgZone, Output, HostBinding, ViewChildren, CUSTOM_ELEMENTS_SCHEMA, model } from '@angular/core';
3
3
  import * as i1$1 from '@angular/forms';
4
4
  import { FormsModule, ControlContainer, NgForm } from '@angular/forms';
5
- import * as i3$4 from '@angular/material/button';
5
+ import * as i3$3 from '@angular/material/button';
6
6
  import { MatIconButton, MatButton, MatButtonModule } from '@angular/material/button';
7
7
  import { MAT_DIALOG_DATA, MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose, MatDialog } from '@angular/material/dialog';
8
- import * as i4$1 from '@angular/material/icon';
8
+ import * as i5$2 from '@angular/material/icon';
9
9
  import { MatIcon, MatIconModule } from '@angular/material/icon';
10
10
  import * as i2$5 from '@angular/material/tooltip';
11
11
  import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
@@ -57,12 +57,12 @@ import { FsDatePickerModule } from '@firestitch/datepicker';
57
57
  import { MatOption } from '@angular/material/core';
58
58
  import { guid } from '@firestitch/common';
59
59
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
60
+ import * as i1$4 from '@firestitch/chip';
61
+ import { FsChipModule } from '@firestitch/chip';
60
62
  import * as i4 from '@firestitch/skeleton';
61
63
  import { FsSkeletonModule } from '@firestitch/skeleton';
62
- import * as i3$3 from '@firestitch/tree';
64
+ import * as i4$1 from '@firestitch/tree';
63
65
  import { FsTreeComponent, TreeDragMode, TreeActionType, FsTreeModule } from '@firestitch/tree';
64
- import * as i1$4 from '@firestitch/chip';
65
- import { FsChipModule } from '@firestitch/chip';
66
66
  import * as i2$4 from '@firestitch/date';
67
67
  import { FsDateModule } from '@firestitch/date';
68
68
  import { FsMarkdownRendererComponent, FsMarkdownEditorComponent } from '@firestitch/markdown-editor';
@@ -123,14 +123,23 @@ class ReportData {
123
123
  ...config,
124
124
  });
125
125
  }
126
- // A real delete the backend refuses while the folder still holds a
127
- // sub-folder or an active report, so there is no undelete counterpart.
126
+ // Soft-delete, whatever the folder holds: its contents leave the listing with
127
+ // it, and undeleteFolder() brings the lot back.
128
128
  deleteFolder(reportFolderId, config = {}) {
129
129
  return this._api.delete(this._path(`folders/${reportFolderId}`), {}, {
130
130
  key: 'folder',
131
131
  ...config,
132
132
  });
133
133
  }
134
+ // Restore a soft-deleted folder through its own action endpoint — the folder
135
+ // PUT does not accept `state`. Any deleted folder above it is restored too, so
136
+ // it lands back where it was.
137
+ undeleteFolder(reportFolderId, config = {}) {
138
+ return this._api.post(this._path(`folders/${reportFolderId}/undelete`), {}, {
139
+ key: 'folder',
140
+ ...config,
141
+ });
142
+ }
134
143
  rename(reportId, name, config = {}) {
135
144
  return this._api.put(this._path(`${reportId}`), { name }, {
136
145
  key: 'report',
@@ -216,7 +225,8 @@ class ReportData {
216
225
  });
217
226
  }
218
227
  // Restore a soft-deleted report through its own action endpoint — the report
219
- // PUT does not accept `state`.
228
+ // PUT does not accept `state`. Its folder is restored too when it is deleted,
229
+ // or inside one that is, so the report lands back where it was.
220
230
  undelete(reportId, config = {}) {
221
231
  return this._api.post(this._path(`${reportId}/undelete`), {}, {
222
232
  key: 'report',
@@ -493,19 +503,15 @@ var ReportNodeType;
493
503
  ReportNodeType["Folder"] = "folder";
494
504
  ReportNodeType["Report"] = "report";
495
505
  })(ReportNodeType || (ReportNodeType = {}));
496
- // Reports are soft-deletable: a deleted report leaves the listing but keeps its
497
- // pages and components, and the listing's "Show Deleted" mode restores it.
498
- // Folders have no equivalent they are only deletable once empty, so they hard
499
- // delete and there is nothing to restore.
506
+ // Reports and folders are both soft-deletable. A deleted report keeps its pages
507
+ // and components; a deleted folder keeps everything filed under it, which leaves
508
+ // the listing with it and comes back with it. "Show Deleted" shows the whole
509
+ // tree, live and deleted together, so any of it can be restored in place.
500
510
  var ReportState;
501
511
  (function (ReportState) {
502
512
  ReportState["Active"] = "active";
503
513
  ReportState["Deleted"] = "deleted";
504
514
  })(ReportState || (ReportState = {}));
505
- const ReportStates = [
506
- { name: 'Active', value: ReportState.Active },
507
- { name: 'Deleted', value: ReportState.Deleted },
508
- ];
509
515
 
510
516
  // A date-range boundary as the picked LOCAL calendar day (no time, no tz).
511
517
  // Date-range filters are calendar-day semantics, so the boundary must carry
@@ -3882,7 +3888,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3882
3888
  // complete synchronously after setOption.
3883
3889
  async function renderChartImage(component, data, width, height, pixelRatio = 3) {
3884
3890
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
3885
- const echarts = (await import('./firestitch-report-echarts-dBElcPnl.mjs')).default;
3891
+ const echarts = (await import('./firestitch-report-echarts-D4xJt3zs.mjs')).default;
3886
3892
  const host = document.createElement('div');
3887
3893
  host.style.width = `${width}px`;
3888
3894
  host.style.height = `${height}px`;
@@ -4970,7 +4976,7 @@ class ReportComponent {
4970
4976
  ReportExportService,
4971
4977
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
4972
4978
  // this route's chunk.
4973
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-dBElcPnl.mjs').then((module) => module.default) }),
4979
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4xJt3zs.mjs').then((module) => module.default) }),
4974
4980
  ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div\n #split\n class=\"report fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n style=\"min-height: 500px;\"\n basePath=\"reports\"\n [requestData]=\"{ reportId: selected?.id ?? null }\"\n [introMessage]=\"introMessage\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n <div class=\"viewer fs-flex fs-column\">\n <div class=\"fs-row.align-center.gap-sm\">\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [fetch]=\"fetchReports\"\n [(ngModel)]=\"selected\"\n [disabled]=\"loadingReports\"\n [padless]=\"true\"\n [multiple]=\"false\"\n [fetchOnFocus]=\"true\"\n (ngModelChange)=\"reportChange($event)\"\n placeholder=\"Report\"\n name=\"report\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n <ng-template\n fsAutocompleteChipsStatic\n (click)=\"createReport()\">\n Create Report\n </ng-template>\n </fs-autocomplete-chips>\n @if (report && editMode) {\n <!-- What layout editing can add or switch to, and the way out of it \u2014\n the same outlined buttons as the fs-ai-report toolbar. -->\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"master-toggle\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n }\n @if (report) {\n <fs-menu>\n <ng-template\n fs-menu-item\n (click)=\"reportSettings()\">\n <mat-icon>\n tune\n </mat-icon>\n Report settings\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleEditMode()\"\n [disabled]=\"editMode\">\n <mat-icon>\n dashboard_customize\n </mat-icon>\n {{ editMode ? 'Done editing layout' : 'Edit layout' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n <mat-icon>\n slideshow\n </mat-icon>\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n <mat-icon>\n picture_as_pdf\n </mat-icon>\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n }\n </div>\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the menu\n above). fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the block\n is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\">\n </app-report-canvas>\n }\n </div>\n</div>", styles: [".report{height:100%;min-height:500px}.report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:500px;min-width:0;border:none}.report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.report .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.report .resizer:before{content:\"\";width:0px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.report .resizer:hover:before{width:3px;background:var(--brand-primary-color)}.report.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.report.resizing .resizer:before{width:3px;background:var(--brand-primary-color)}.report.resizing .chat,.report.resizing .viewer{pointer-events:none}::ng-deep body.body-report-reports .mat-mdc-card-content{display:flex;flex-direction:column;box-sizing:border-box}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel{flex:1;min-height:0}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel router-outlet+ng-component{height:100%}.master-toggle.active{background:#eceff3}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$3.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$3.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$3.FsAutocompleteChipsStaticDirective, selector: "[fsAutocompleteChipsStatic]", inputs: ["show", "disable"], outputs: ["click", "selected"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$1.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$1.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4975
4981
  }
4976
4982
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
@@ -4985,7 +4991,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4985
4991
  ReportExportService,
4986
4992
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
4987
4993
  // this route's chunk.
4988
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-dBElcPnl.mjs').then((module) => module.default) }),
4994
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4xJt3zs.mjs').then((module) => module.default) }),
4989
4995
  ], imports: [
4990
4996
  FormsModule,
4991
4997
  FsAutocompleteChipsModule,
@@ -5662,17 +5668,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
5662
5668
  // report in no folder is a root node, which is where every report sat before
5663
5669
  // folders existed, so nothing had to be migrated or bucketed as "uncategorized".
5664
5670
  //
5665
- // Reports are soft-deletable: the row kebab deletes, "Show Deleted" flips the
5666
- // listing into deleted mode (the backend hides deleted reports otherwise), and
5667
- // in that mode the kebab offers Restore instead. Folders do NOT take part in
5668
- // that a folder is only deletable once it is empty, so it hard deletes and has
5669
- // nothing to restore. The upshot is that deleted mode keeps the folder structure
5670
- // on screen with the deleted reports sitting where they belong, so restoring one
5671
- // puts it back rather than dropping it at the root.
5671
+ // Folders and reports are both soft-deletable, and a folder deletes whatever it
5672
+ // holds. Only the folder's own row is written: everything under it drops out of
5673
+ // the listing because it sits under a deleted folder, and comes back with it.
5674
+ // "Show Deleted" shows the WHOLE tree live and deleted together, each deleted
5675
+ // row where it was filed and on anything deleted the kebab offers Restore
5676
+ // instead. Restoring also restores any deleted folder above the row, so it lands
5677
+ // back where it was rather than at the root.
5678
+ //
5679
+ // Every folder starts collapsed; the tree remembers what was opened across its
5680
+ // own reloads (a rename, a move, the Show Deleted toggle) and across a search.
5672
5681
  class FsAiReportsComponent {
5673
- // How deep the tree opens on load. Folders are audience buckets, not a
5674
- // filesystem, so there is nothing worth hiding behind a closed chevron.
5675
- static EXPAND_LEVEL = 10;
5676
5682
  // API base for the reports endpoints; override to mount elsewhere.
5677
5683
  basePath = input('reports');
5678
5684
  // Where a row links to; ':id' is replaced with the report id. Default takes
@@ -5696,6 +5702,10 @@ class FsAiReportsComponent {
5696
5702
  _destroyRef = inject(DestroyRef);
5697
5703
  // The filter bar's current query — carries `state` when Show Deleted is on.
5698
5704
  _query = {};
5705
+ // The folders that were open when a search started; null while no search is
5706
+ // running. The search opens every match's path over the top of them, and
5707
+ // clearing it puts these back.
5708
+ _expandedBeforeSearch = null;
5699
5709
  constructor() {
5700
5710
  effect(() => {
5701
5711
  this._reportData.basePath = this.basePath();
@@ -5713,20 +5723,16 @@ class FsAiReportsComponent {
5713
5723
  openLink(report) {
5714
5724
  return this._patternCommands(this.openPattern(), report.id);
5715
5725
  }
5716
- // A deleted report can't be openedthe backend only assembles active ones —
5717
- // so in Show Deleted mode its name renders as plain text rather than a link.
5718
- isDeleted(report) {
5719
- return report?.state === ReportState.Deleted;
5720
- }
5721
- // Prompt for a title, create the report, then route to it (createPattern).
5722
- createReport() {
5726
+ // Prompt for a title, create the report filed under reportFolderId when a
5727
+ // folder's kebab asked, at the root otherwise then route to it (createPattern).
5728
+ createReport(reportFolderId = null) {
5723
5729
  this._prompt.input({
5724
5730
  title: 'Create Report',
5725
5731
  label: 'Title',
5726
5732
  commitLabel: 'Create',
5727
5733
  required: true,
5728
5734
  })
5729
- .pipe(switchMap((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
5735
+ .pipe(switchMap((name) => this._reportData.create(name, reportFolderId)), takeUntilDestroyed(this._destroyRef))
5730
5736
  .subscribe((report) => {
5731
5737
  const pattern = this.createPattern();
5732
5738
  this._router.navigate(this._patternCommands(pattern, report.id), pattern.startsWith('/') ? {} : { relativeTo: this._route });
@@ -5751,13 +5757,16 @@ class FsAiReportsComponent {
5751
5757
  type: ItemType.Keyword,
5752
5758
  label: 'Search',
5753
5759
  },
5760
+ // A checkbox rather than the usual Status select with a "Show Deleted"
5761
+ // isolate. Isolating the deleted rows would strand them: a deleted
5762
+ // report in a live folder arrives without the folder it sits in, and a
5763
+ // tree cannot place a node whose parent it never received. So the
5764
+ // toggle asks for everything, and the rows mark what is deleted.
5754
5765
  {
5755
5766
  name: 'state',
5756
- type: ItemType.Select,
5757
- label: 'Status',
5758
- multiple: true,
5759
- values: () => ReportStates,
5760
- isolate: { label: 'Show Deleted', value: ReportState.Deleted },
5767
+ type: ItemType.Checkbox,
5768
+ label: 'Show Deleted',
5769
+ checked: [ReportState.Active, ReportState.Deleted].join(','),
5761
5770
  },
5762
5771
  ],
5763
5772
  actions: [
@@ -5790,15 +5799,19 @@ class FsAiReportsComponent {
5790
5799
  click: () => this.createReport(),
5791
5800
  },
5792
5801
  ],
5793
- // The two items are answered in different places and both have to run.
5794
- // The keyword drives the tree's own search, which is the whole point of
5795
- // searching a tree — it reveals a match nested inside collapsed folders
5796
- // rather than only filtering what is already on screen. `state` is the
5797
- // Show Deleted toggle, and only the server can answer that.
5802
+ // The two items are answered in different places, and each only when it
5803
+ // changed. The keyword drives the tree's own search, which is the whole
5804
+ // point of searching a tree — it reveals a match nested inside collapsed
5805
+ // folders rather than only filtering what is already on screen. `state` is
5806
+ // the Show Deleted toggle, and only the server can answer that. Running the
5807
+ // search on a toggle would clear it, and clearing collapses the tree.
5798
5808
  change: (query) => {
5809
+ const keywordChanged = String(query?.keyword ?? '') !== String(this._query.keyword ?? '');
5799
5810
  const stateChanged = String(query?.state ?? '') !== String(this._query.state ?? '');
5800
5811
  this._query = { ...query };
5801
- this._search(query?.keyword);
5812
+ if (keywordChanged) {
5813
+ this._search(query?.keyword);
5814
+ }
5802
5815
  if (stateChanged) {
5803
5816
  this._load();
5804
5817
  }
@@ -5806,22 +5819,26 @@ class FsAiReportsComponent {
5806
5819
  };
5807
5820
  }
5808
5821
  _initTreeConfig() {
5822
+ // No expandLevel: every folder starts collapsed.
5809
5823
  this.treeConfig = {
5810
5824
  data: [],
5811
5825
  childrenName: 'children',
5812
- expandLevel: FsAiReportsComponent.EXPAND_LEVEL,
5813
5826
  draggable: true,
5814
5827
  // The whole row is the grip and the handle stays hidden; a press only
5815
5828
  // becomes a drag once the pointer moves, so the kebab, the chevron and the
5816
5829
  // report link all stay clickable.
5817
5830
  dragMode: TreeDragMode.Node,
5831
+ // Anything deleted is restored before it is moved — the backend refuses
5832
+ // to edit a deleted row, and a drag would only snap back.
5833
+ canDrag: (node) => !this._isDeletedNode(node),
5818
5834
  // canDrop is asked about the node being DRAGGED, so only `toParent`
5819
5835
  // describes the destination — reading node.level/node.parent here would
5820
5836
  // describe where it came from. A report can never be a parent because its
5821
5837
  // type says so, and that single test is the whole "folders nest, reports
5822
- // are leaves" rule on this side.
5838
+ // are leaves" rule on this side. A deleted folder is no destination
5839
+ // either: whatever landed in it would vanish from the live listing.
5823
5840
  canDrop: (node, fromParent, toParent) => {
5824
- return !toParent || toParent.data?.type === ReportNodeType.Folder;
5841
+ return !toParent || (this._isFolderNode(toParent) && !this._isDeletedNode(toParent));
5825
5842
  },
5826
5843
  changeReorder: (event) => this._move(event),
5827
5844
  nodeClass: (node) => node.data?.type,
@@ -5844,8 +5861,13 @@ class FsAiReportsComponent {
5844
5861
  click: (node) => this._rename(node),
5845
5862
  },
5846
5863
  {
5847
- label: 'New Folder Inside',
5848
- show: (node) => this._isFolderNode(node),
5864
+ label: 'New report',
5865
+ show: (node) => this._isFolderNode(node) && !this._isDeletedNode(node),
5866
+ click: (node) => this.createReport(node.data?.id),
5867
+ },
5868
+ {
5869
+ label: 'New folder',
5870
+ show: (node) => this._isFolderNode(node) && !this._isDeletedNode(node),
5849
5871
  click: (node) => this._createFolder(node.data?.id),
5850
5872
  },
5851
5873
  {
@@ -5853,8 +5875,8 @@ class FsAiReportsComponent {
5853
5875
  show: (node) => !this._isDeletedNode(node),
5854
5876
  click: (node) => this._delete(node),
5855
5877
  },
5856
- // Folders never reach this: they hard delete, so a deleted folder
5857
- // does not exist to be restored.
5878
+ // On every deleted row, including a live one that is only hidden by
5879
+ // a deleted folder above it — restoring that brings the folder back.
5858
5880
  {
5859
5881
  label: 'Restore',
5860
5882
  show: (node) => this._isDeletedNode(node),
@@ -5869,66 +5891,128 @@ class FsAiReportsComponent {
5869
5891
  return node?.data?.type === ReportNodeType.Folder;
5870
5892
  }
5871
5893
  _isDeletedNode(node) {
5872
- return this.isDeleted(node?.data?.report);
5894
+ return !!node?.data?.deleted;
5873
5895
  }
5874
5896
  // Drive the tree's built-in search: it collapses everything, hides every node,
5875
5897
  // then unhides each match and walks its ancestors open, so a report several
5876
- // folders deep surfaces with its path expanded. Clearing does NOT undo that
5877
- // initial collapse, so the tree is re-opened here otherwise the listing is
5878
- // left fully collapsed the moment somebody empties the search box.
5898
+ // folders deep surfaces with its path expanded. Clearing collapses everything
5899
+ // again, so the folders that were open before the search are put back.
5879
5900
  _search(keyword) {
5880
5901
  const tree = this._tree();
5881
5902
  if (!tree) {
5882
5903
  return;
5883
5904
  }
5884
5905
  if (keyword) {
5906
+ this._expandedBeforeSearch ??= this._expandedKeys();
5885
5907
  tree.search(keyword);
5886
5908
  return;
5887
5909
  }
5888
5910
  tree.clearSearch();
5889
- tree.expandAll();
5911
+ this._expandKeys(this._expandedBeforeSearch ?? []);
5912
+ this._expandedBeforeSearch = null;
5890
5913
  }
5891
5914
  _load() {
5892
- this._reportData.tree(this._query)
5915
+ // Only the Show Deleted toggle is the server's to answer — the keyword is
5916
+ // the tree's own search — and an unchecked toggle sends nothing, leaving the
5917
+ // backend's active-only default in charge.
5918
+ const query = this._query.state ? { state: this._query.state } : {};
5919
+ this._reportData.tree(query)
5893
5920
  .pipe(takeUntilDestroyed(this._destroyRef))
5894
- .subscribe((tree) => {
5895
- const nodes = this._buildNodes(tree);
5921
+ .subscribe((reportTree) => {
5922
+ const nodes = this._buildNodes(reportTree);
5896
5923
  this.nodes.set(nodes);
5897
5924
  // The tree reads `config` once, in its own ngOnInit. The first load
5898
5925
  // lands before it exists, so the data has to arrive through the config;
5899
5926
  // every later one has to go through the instance or nothing changes.
5900
5927
  this.treeConfig = { ...this.treeConfig, data: nodes };
5901
- this._tree()?.setData(nodes);
5928
+ const tree = this._tree();
5929
+ if (!tree) {
5930
+ return;
5931
+ }
5932
+ // setData() rebuilds every node collapsed, so a rename or a move would
5933
+ // otherwise slam shut every folder somebody had opened. Keys are stable
5934
+ // across loads ('f:12'), so what was open is reopened by key — and a
5935
+ // folder that no longer comes back simply is not.
5936
+ const expandedKeys = this._expandedKeys();
5937
+ tree.setData(nodes);
5938
+ this._expandKeys(expandedKeys);
5939
+ const keyword = String(this._query.keyword ?? '');
5940
+ if (keyword) {
5941
+ tree.search(keyword);
5942
+ }
5902
5943
  });
5903
5944
  }
5945
+ // The keys of every open folder, walked through the tree's public node API.
5946
+ _expandedKeys() {
5947
+ const tree = this._tree();
5948
+ if (!tree) {
5949
+ return [];
5950
+ }
5951
+ const walk = (parent) => tree.getNodes(parent)
5952
+ .filter((node) => node.isExpanded())
5953
+ .flatMap((node) => [node.data?.key, ...walk(node)]);
5954
+ return walk(null);
5955
+ }
5956
+ _expandKeys(keys) {
5957
+ const tree = this._tree();
5958
+ if (!tree || !keys.length) {
5959
+ return;
5960
+ }
5961
+ const open = new Set(keys);
5962
+ const walk = (parent) => {
5963
+ tree.getNodes(parent)
5964
+ .filter((node) => open.has(node.data?.key))
5965
+ .forEach((node) => {
5966
+ node.expand();
5967
+ walk(node);
5968
+ });
5969
+ };
5970
+ walk(null);
5971
+ }
5904
5972
  // Flat API rows → the nested shape fs-tree renders. Folders come before
5905
5973
  // reports at every level, each already name-ordered by the backend.
5906
- _buildNodes(tree) {
5907
- const folders = tree?.folders ?? [];
5908
- const reports = tree?.reports ?? [];
5909
- const build = (parentId) => {
5974
+ //
5975
+ // `deleted` is inherited downward: a deleted folder hides everything below it
5976
+ // without the rows below being deleted themselves, and that is only visible
5977
+ // from here, where the parent is in hand. The backend applies the same rule
5978
+ // when it filters (ReportFolderModel::getDeletedFolderIds()).
5979
+ _buildNodes(reportTree) {
5980
+ const folders = reportTree?.folders ?? [];
5981
+ const reports = reportTree?.reports ?? [];
5982
+ const build = (parentId, parentDeleted) => {
5910
5983
  const folderNodes = folders
5911
5984
  .filter((folder) => (folder.parentReportFolderId ?? null) === parentId)
5912
- .map((folder) => ({
5913
- key: `f:${folder.id}`,
5914
- id: folder.id,
5915
- name: folder.name,
5916
- type: ReportNodeType.Folder,
5917
- children: build(folder.id),
5918
- }));
5985
+ .map((folder) => {
5986
+ const stateDeleted = folder.state === ReportState.Deleted;
5987
+ const deleted = parentDeleted || stateDeleted;
5988
+ return {
5989
+ key: `f:${folder.id}`,
5990
+ id: folder.id,
5991
+ name: folder.name,
5992
+ type: ReportNodeType.Folder,
5993
+ children: build(folder.id, deleted),
5994
+ stateDeleted,
5995
+ deleted,
5996
+ };
5997
+ });
5919
5998
  const reportNodes = reports
5920
5999
  .filter((report) => (report.reportFolderId ?? null) === parentId)
5921
- .map((report) => ({
5922
- key: `r:${report.id}`,
5923
- id: report.id,
5924
- name: report.name,
5925
- type: ReportNodeType.Report,
5926
- children: [],
5927
- report,
5928
- }));
6000
+ .map((report) => {
6001
+ const stateDeleted = report.state === ReportState.Deleted;
6002
+ return {
6003
+ key: `r:${report.id}`,
6004
+ id: report.id,
6005
+ name: report.name,
6006
+ type: ReportNodeType.Report,
6007
+ children: [],
6008
+ report,
6009
+ stateDeleted,
6010
+ deleted: parentDeleted || stateDeleted,
6011
+ };
6012
+ });
5929
6013
  return [...folderNodes, ...reportNodes];
5930
6014
  };
5931
- return build(null);
6015
+ return build(null, false);
5932
6016
  }
5933
6017
  // A drop moved a node under a new parent, or out to the root. The tree has
5934
6018
  // already moved it on screen, so both outcomes reload: a success to pick up
@@ -5989,9 +6073,9 @@ class FsAiReportsComponent {
5989
6073
  this._load();
5990
6074
  });
5991
6075
  }
5992
- // Deleting a folder is refused server-side while it still holds a sub-folder
5993
- // or an active report, and the message says which so the confirm here warns
5994
- // that a folder delete is permanent rather than trying to restate the rule.
6076
+ // A folder deletes with everything in it, so the confirm says what that is —
6077
+ // and that it all comes back from Show Deleted, which is what makes it safe to
6078
+ // offer.
5995
6079
  _delete(node) {
5996
6080
  const data = node?.data;
5997
6081
  if (!data) {
@@ -6001,7 +6085,7 @@ class FsAiReportsComponent {
6001
6085
  this._prompt.confirm({
6002
6086
  title: 'Confirm',
6003
6087
  template: folder
6004
- ? `Are you sure you would like to delete the folder "${data.name}"? This cannot be undone.`
6088
+ ? this._deleteFolderMessage(data)
6005
6089
  : 'Are you sure you would like to delete this report?',
6006
6090
  })
6007
6091
  .pipe(switchMap(() => (folder
@@ -6012,15 +6096,41 @@ class FsAiReportsComponent {
6012
6096
  this._load();
6013
6097
  });
6014
6098
  }
6099
+ // Counted from the node already on screen, live rows only: in Show Deleted
6100
+ // mode the folder can also hold rows that are gone already, and those are not
6101
+ // leaving with it.
6102
+ _deleteFolderMessage(folder) {
6103
+ const count = (node) => node.children
6104
+ .filter((child) => !child.deleted)
6105
+ .reduce((total, child) => {
6106
+ const below = count(child);
6107
+ return {
6108
+ folders: total.folders + below.folders + (child.type === ReportNodeType.Folder ? 1 : 0),
6109
+ reports: total.reports + below.reports + (child.type === ReportNodeType.Report ? 1 : 0),
6110
+ };
6111
+ }, { folders: 0, reports: 0 });
6112
+ const { folders, reports } = count(folder);
6113
+ const contents = [
6114
+ folders ? `${folders} ${folders === 1 ? 'folder' : 'folders'}` : '',
6115
+ reports ? `${reports} ${reports === 1 ? 'report' : 'reports'}` : '',
6116
+ ].filter(Boolean);
6117
+ const inside = contents.length ? ` and the ${contents.join(' and ')} inside it` : '';
6118
+ return `Are you sure you would like to delete the folder "${folder.name}"${inside}? It can be restored from Show Deleted.`;
6119
+ }
6120
+ // Both endpoints also restore any deleted folder above the row, so a row that
6121
+ // is live itself but hidden by a deleted folder is restored by the same call.
6015
6122
  _restore(node) {
6016
6123
  const data = node?.data;
6017
6124
  if (!data) {
6018
6125
  return;
6019
6126
  }
6020
- this._reportData.undelete(data.id)
6127
+ const folder = data.type === ReportNodeType.Folder;
6128
+ (folder
6129
+ ? this._reportData.undeleteFolder(data.id)
6130
+ : this._reportData.undelete(data.id))
6021
6131
  .pipe(takeUntilDestroyed(this._destroyRef))
6022
6132
  .subscribe(() => {
6023
- this._message.success('Restored report');
6133
+ this._message.success(folder ? 'Restored folder' : 'Restored report');
6024
6134
  this._load();
6025
6135
  });
6026
6136
  }
@@ -6040,17 +6150,18 @@ class FsAiReportsComponent {
6040
6150
  return segments;
6041
6151
  }
6042
6152
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6043
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "_tree", first: true, predicate: FsTreeComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div class=\"fs-row.gap-sm.align-center node-row\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder) {\n <span class=\"node-name\">{{ data.name }}</span>\n } @else if (isDeleted(data.report)) {\n <!-- A deleted report can't be opened (the backend only assembles\n active ones), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i3$3.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i3$3.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6153
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "_tree", first: true, predicate: FsTreeComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div\n class=\"fs-row.gap-sm.align-center node-row\"\n [class.deleted]=\"data.deleted\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder || data.deleted) {\n <!-- A deleted report can't be opened (the backend only assembles\n live ones, and one under a deleted folder is not on the live\n listing), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n\n <!-- Only on the row somebody deleted. Everything under a deleted\n folder is muted with it, but was not deleted itself. -->\n @if (data.stateDeleted) {\n <fs-chip size=\"tiny\">Deleted</fs-chip>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-row.deleted{opacity:.55}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"], dependencies: [{ kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1$4.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i4$1.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i4$1.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6044
6154
  }
6045
6155
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
6046
6156
  type: Component,
6047
6157
  args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
6158
+ FsChipModule,
6048
6159
  FsFilterModule,
6049
6160
  FsSkeletonModule,
6050
6161
  FsTreeModule,
6051
6162
  MatIconModule,
6052
6163
  RouterLink,
6053
- ], 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"] }]
6164
+ ], template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div\n class=\"fs-row.gap-sm.align-center node-row\"\n [class.deleted]=\"data.deleted\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder || data.deleted) {\n <!-- A deleted report can't be opened (the backend only assembles\n live ones, and one under a deleted folder is not on the live\n listing), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n\n <!-- Only on the row somebody deleted. Everything under a deleted\n folder is muted with it, but was not deleted itself. -->\n @if (data.stateDeleted) {\n <fs-chip size=\"tiny\">Deleted</fs-chip>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-row.deleted{opacity:.55}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"] }]
6054
6165
  }], ctorParameters: () => [] });
6055
6166
 
6056
6167
  // A single report: the AI chat on the left, the report canvas on the right,
@@ -6432,8 +6543,8 @@ class FsAiReportComponent {
6432
6543
  ReportPdfService,
6433
6544
  ReportExportService,
6434
6545
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
6435
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-dBElcPnl.mjs').then((module) => module.default) }),
6436
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode\"\n [matTooltip]=\"editMode ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n [pageId]=\"pageId\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$1.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$1.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$1.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$4.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$4.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6546
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4xJt3zs.mjs').then((module) => module.default) }),
6547
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode\"\n [matTooltip]=\"editMode ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n [pageId]=\"pageId\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$1.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$1.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$1.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6437
6548
  }
6438
6549
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
6439
6550
  type: Component,
@@ -6446,7 +6557,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6446
6557
  ReportPdfService,
6447
6558
  ReportExportService,
6448
6559
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
6449
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-dBElcPnl.mjs').then((module) => module.default) }),
6560
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4xJt3zs.mjs').then((module) => module.default) }),
6450
6561
  ], imports: [
6451
6562
  FsFilterModule,
6452
6563
  FsMenuModule,
@@ -6479,5 +6590,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
6479
6590
  * Generated bundle index. Do not edit.
6480
6591
  */
6481
6592
 
6482
- export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, ReportNodeType as f, ReportState as g, ReportStates as h };
6483
- //# sourceMappingURL=firestitch-report-firestitch-report-DhxGcHlU.mjs.map
6593
+ export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, ReportNodeType as f, ReportState as g };
6594
+ //# sourceMappingURL=firestitch-report-firestitch-report-DY8HwvAU.mjs.map