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