@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.
- package/app/reports/data/report.data.d.ts +1 -0
- package/app/reports/interfaces/report.interface.d.ts +3 -4
- package/app/reports/views/reports/fs-ai-reports.component.d.ts +5 -3
- package/esm2022/app/reports/data/report.data.mjs +14 -4
- package/esm2022/app/reports/interfaces/report.interface.mjs +5 -9
- package/esm2022/app/reports/views/reports/fs-ai-reports.component.mjs +186 -79
- package/fesm2022/{firestitch-report-echarts-dBElcPnl.mjs → firestitch-report-echarts-D4xJt3zs.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-dBElcPnl.mjs.map → firestitch-report-echarts-D4xJt3zs.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-DhxGcHlU.mjs → firestitch-report-firestitch-report-DY8HwvAU.mjs} +208 -97
- package/fesm2022/firestitch-report-firestitch-report-DY8HwvAU.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/firestitch-report-firestitch-report-DhxGcHlU.mjs.map +0 -1
|
@@ -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$
|
|
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
|
|
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
|
|
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
|
-
//
|
|
127
|
-
//
|
|
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
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
//
|
|
5666
|
-
//
|
|
5667
|
-
//
|
|
5668
|
-
//
|
|
5669
|
-
//
|
|
5670
|
-
//
|
|
5671
|
-
//
|
|
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
|
-
//
|
|
5717
|
-
//
|
|
5718
|
-
|
|
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.
|
|
5757
|
-
label: '
|
|
5758
|
-
|
|
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
|
|
5794
|
-
// The keyword drives the tree's own search, which is the whole
|
|
5795
|
-
// searching a tree — it reveals a match nested inside collapsed
|
|
5796
|
-
// rather than only filtering what is already on screen. `state` is
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
5857
|
-
//
|
|
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
|
|
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
|
|
5877
|
-
//
|
|
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
|
-
|
|
5911
|
+
this._expandKeys(this._expandedBeforeSearch ?? []);
|
|
5912
|
+
this._expandedBeforeSearch = null;
|
|
5890
5913
|
}
|
|
5891
5914
|
_load() {
|
|
5892
|
-
|
|
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((
|
|
5895
|
-
const nodes = this._buildNodes(
|
|
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()
|
|
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
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
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
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
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
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
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
|
-
//
|
|
5993
|
-
//
|
|
5994
|
-
//
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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$
|
|
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-
|
|
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
|
|
6483
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
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
|