@firestitch/report 18.0.18 → 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/dialogs/global-settings/components/semantic-database/semantic-database.component.d.ts +5 -3
- package/app/reports/export/report-export-collector.service.d.ts +1 -0
- package/app/reports/format.d.ts +1 -1
- package/app/reports/interfaces/report.interface.d.ts +27 -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/components/component-kpi/component-kpi.component.mjs +2 -2
- package/esm2022/app/reports/data/report.data.mjs +51 -3
- package/esm2022/app/reports/dialogs/global-settings/components/semantic-database/semantic-database.component.mjs +11 -6
- package/esm2022/app/reports/export/report-export-collector.service.mjs +31 -11
- package/esm2022/app/reports/export/report-pdf.service.mjs +2 -2
- package/esm2022/app/reports/export/report-pptx.service.mjs +2 -2
- package/esm2022/app/reports/format.mjs +21 -6
- package/esm2022/app/reports/interfaces/report.interface.mjs +12 -1
- package/esm2022/app/reports/services/report-filter-state.service.mjs +28 -2
- 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-CMOBB-Er.mjs → firestitch-report-echarts-D4VLD05J.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-CMOBB-Er.mjs.map → firestitch-report-echarts-D4VLD05J.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-DwgbMhwr.mjs → firestitch-report-firestitch-report-D5cx4IJg.mjs} +535 -108
- 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-DwgbMhwr.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';
|
|
@@ -27,7 +27,7 @@ import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
|
|
|
27
27
|
import { FsApi, StreamEventType } from '@firestitch/api';
|
|
28
28
|
import * as i1$1 from '@angular/material/menu';
|
|
29
29
|
import { MatMenuModule } from '@angular/material/menu';
|
|
30
|
-
import { format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, parseISO, isValid } from 'date-fns';
|
|
30
|
+
import { format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek, parseISO, isValid } from 'date-fns';
|
|
31
31
|
import { DatePipe } from '@angular/common';
|
|
32
32
|
import * as i1 from '@firestitch/list';
|
|
33
33
|
import { PaginationStrategy, FsListComponent, FsListModule } from '@firestitch/list';
|
|
@@ -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
|
|
@@ -629,6 +690,21 @@ class ReportFilterStateService {
|
|
|
629
690
|
_relativeRange(relative) {
|
|
630
691
|
const now = new Date();
|
|
631
692
|
switch (relative) {
|
|
693
|
+
// A calendar week, Sunday to Saturday, in whole days — not the rolling
|
|
694
|
+
// seven days `last7Days` gives, which never lines up with the week a
|
|
695
|
+
// partner has already been sent. `lastWeek` is the one a weekly report
|
|
696
|
+
// wants: it ends at endOfWeek rather than at `now`, because the week it
|
|
697
|
+
// names has closed. `thisWeek` follows every other preset here and runs
|
|
698
|
+
// up to now, so it is deliberately a part-finished week.
|
|
699
|
+
case 'thisWeek':
|
|
700
|
+
return { start: startOfWeek(now, { weekStartsOn: 0 }), end: now };
|
|
701
|
+
case 'lastWeek': {
|
|
702
|
+
const previous = subWeeks(now, 1);
|
|
703
|
+
return {
|
|
704
|
+
start: startOfWeek(previous, { weekStartsOn: 0 }),
|
|
705
|
+
end: endOfWeek(previous, { weekStartsOn: 0 }),
|
|
706
|
+
};
|
|
707
|
+
}
|
|
632
708
|
case 'last7Days':
|
|
633
709
|
return { start: subDays(now, 7), end: now };
|
|
634
710
|
case 'last30Days':
|
|
@@ -993,9 +1069,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
993
1069
|
}] } });
|
|
994
1070
|
|
|
995
1071
|
// Measure-value formatting shared by the KPI renderer and the exports — the
|
|
996
|
-
// config's `format` (number | percent | currency)
|
|
997
|
-
// screen, in PowerPoint and in PDF.
|
|
998
|
-
|
|
1072
|
+
// config's `format` (number | percent | currency) and `precision` mean the same
|
|
1073
|
+
// thing on screen, in PowerPoint and in PDF.
|
|
1074
|
+
// A bare `new Intl.NumberFormat()` defaults to maximumFractionDigits: 3, which
|
|
1075
|
+
// is why an unconfigured percentage reads 38.298%. When the measure names a
|
|
1076
|
+
// precision it is pinned as BOTH the minimum and the maximum, so a column of
|
|
1077
|
+
// rates lines up instead of each value showing however many decimals it happens
|
|
1078
|
+
// to need. Undefined leaves the locale default alone, so a report authored
|
|
1079
|
+
// before precision existed renders exactly as it did.
|
|
1080
|
+
function getFractionDigits(precision) {
|
|
1081
|
+
return Number.isFinite(precision)
|
|
1082
|
+
? { minimumFractionDigits: precision, maximumFractionDigits: precision }
|
|
1083
|
+
: {};
|
|
1084
|
+
}
|
|
1085
|
+
function formatMeasureValue(value, format, precision) {
|
|
999
1086
|
if (value === null || value === undefined || value === '') {
|
|
1000
1087
|
return '—';
|
|
1001
1088
|
}
|
|
@@ -1003,17 +1090,21 @@ function formatMeasureValue(value, format) {
|
|
|
1003
1090
|
if (!Number.isFinite(numeric)) {
|
|
1004
1091
|
return String(value);
|
|
1005
1092
|
}
|
|
1093
|
+
const fractionDigits = getFractionDigits(precision);
|
|
1006
1094
|
switch (format) {
|
|
1007
1095
|
case 'percent':
|
|
1008
|
-
return `${new Intl.NumberFormat().format(numeric)}%`;
|
|
1096
|
+
return `${new Intl.NumberFormat(undefined, fractionDigits).format(numeric)}%`;
|
|
1009
1097
|
case 'currency':
|
|
1010
1098
|
return new Intl.NumberFormat(undefined, {
|
|
1011
1099
|
style: 'currency',
|
|
1012
1100
|
currency: 'USD',
|
|
1101
|
+
// Whole dollars unless the measure asks for decimals — cents were
|
|
1102
|
+
// unrepresentable before, because this was hardcoded to 0.
|
|
1013
1103
|
maximumFractionDigits: 0,
|
|
1104
|
+
...fractionDigits,
|
|
1014
1105
|
}).format(numeric);
|
|
1015
1106
|
default:
|
|
1016
|
-
return new Intl.NumberFormat().format(numeric);
|
|
1107
|
+
return new Intl.NumberFormat(undefined, fractionDigits).format(numeric);
|
|
1017
1108
|
}
|
|
1018
1109
|
}
|
|
1019
1110
|
|
|
@@ -1033,7 +1124,7 @@ class ComponentKpiComponent {
|
|
|
1033
1124
|
return;
|
|
1034
1125
|
}
|
|
1035
1126
|
const column = config.measure?.column ?? Object.keys(row)[0];
|
|
1036
|
-
this.value = formatMeasureValue(row[column], config.measure?.format);
|
|
1127
|
+
this.value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
|
|
1037
1128
|
}
|
|
1038
1129
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentKpiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1039
1130
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentKpiComponent, isStandalone: true, selector: "app-report-component-kpi", inputs: { component: "component", data: "data" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"kpi\">\n <div class=\"kpi-value\">{{ value }}</div>\n @if (label) {\n <div class=\"kpi-label\">{{ label }}</div>\n }\n</div>\n", styles: [":host{display:block;width:100%;height:100%}.kpi{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px}.kpi .kpi-value{font-size:34px;font-weight:600;color:#1f2933;line-height:1}.kpi .kpi-label{font-size:12px;color:#7b8794}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
@@ -2498,13 +2589,24 @@ const FREQUENCY_OPTIONS = [
|
|
|
2498
2589
|
{ value: 'quarter', name: 'Quarter' },
|
|
2499
2590
|
{ value: 'year', name: 'Annually' },
|
|
2500
2591
|
];
|
|
2592
|
+
var ReportNodeType;
|
|
2593
|
+
(function (ReportNodeType) {
|
|
2594
|
+
ReportNodeType["Folder"] = "folder";
|
|
2595
|
+
ReportNodeType["Report"] = "report";
|
|
2596
|
+
})(ReportNodeType || (ReportNodeType = {}));
|
|
2501
2597
|
// Reports are soft-deletable: a deleted report leaves the listing but keeps its
|
|
2502
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.
|
|
2503
2601
|
var ReportState;
|
|
2504
2602
|
(function (ReportState) {
|
|
2505
2603
|
ReportState["Active"] = "active";
|
|
2506
2604
|
ReportState["Deleted"] = "deleted";
|
|
2507
2605
|
})(ReportState || (ReportState = {}));
|
|
2606
|
+
const ReportStates = [
|
|
2607
|
+
{ name: 'Active', value: ReportState.Active },
|
|
2608
|
+
{ name: 'Deleted', value: ReportState.Deleted },
|
|
2609
|
+
];
|
|
2508
2610
|
|
|
2509
2611
|
// Rows fetched for a list component's export table — enough to fill a
|
|
2510
2612
|
// slide/page table; the cap note covers the rest.
|
|
@@ -2526,8 +2628,10 @@ class ReportExportCollectorService {
|
|
|
2526
2628
|
}
|
|
2527
2629
|
});
|
|
2528
2630
|
const all$ = requests.length ? forkJoin(requests) : of([]);
|
|
2529
|
-
|
|
2530
|
-
|
|
2631
|
+
// The summary rides alongside the component data rather than being built at
|
|
2632
|
+
// the end, because naming a select filter's values is itself a request.
|
|
2633
|
+
return forkJoin({ collected: all$, filterSummary: this._filterSummary(report) })
|
|
2634
|
+
.pipe(map$1(({ collected, filterSummary }) => {
|
|
2531
2635
|
const pages = report.pages.map((page) => ({ page, components: [] }));
|
|
2532
2636
|
collected.forEach((item, index) => {
|
|
2533
2637
|
pages[slots[index].pageIndex].components.push(item);
|
|
@@ -2535,7 +2639,7 @@ class ReportExportCollectorService {
|
|
|
2535
2639
|
return {
|
|
2536
2640
|
report,
|
|
2537
2641
|
pages,
|
|
2538
|
-
filterSummary
|
|
2642
|
+
filterSummary,
|
|
2539
2643
|
};
|
|
2540
2644
|
}));
|
|
2541
2645
|
}
|
|
@@ -2551,8 +2655,12 @@ class ReportExportCollectorService {
|
|
|
2551
2655
|
return this._reportData.componentData(report.id, component.id, filters, state)
|
|
2552
2656
|
.pipe(map$1((data) => ({ component, data })), catchError(() => of({ component, data: null })));
|
|
2553
2657
|
}
|
|
2554
|
-
// "Report Period: Jun 12, 2025 – Jun 12, 2026", "Organization:
|
|
2555
|
-
// line per filter group that has an active session value.
|
|
2658
|
+
// "Report Period: Jun 12, 2025 – Jun 12, 2026", "Organization: Alnylam - AHP,
|
|
2659
|
+
// Alnylam - PH1" — one line per filter group that has an active session value.
|
|
2660
|
+
//
|
|
2661
|
+
// A select group stores ids, so its line has to be named off the option list
|
|
2662
|
+
// before it can be printed; a date or keyword group already holds its own
|
|
2663
|
+
// display value and resolves synchronously.
|
|
2556
2664
|
_filterSummary(report) {
|
|
2557
2665
|
const lines = [];
|
|
2558
2666
|
for (const group of report.filterGroups ?? []) {
|
|
@@ -2562,21 +2670,34 @@ class ReportExportCollectorService {
|
|
|
2562
2670
|
}
|
|
2563
2671
|
const label = group.label || 'Filter';
|
|
2564
2672
|
if (value.start || value.end) {
|
|
2565
|
-
lines.push(`${label}: ${this._date(value.start)} – ${this._date(value.end)}`);
|
|
2673
|
+
lines.push(of(`${label}: ${this._date(value.start)} – ${this._date(value.end)}`));
|
|
2566
2674
|
}
|
|
2567
2675
|
else if (value.values?.length) {
|
|
2568
|
-
lines.push(
|
|
2676
|
+
lines.push(this._selectLine(label, group, report.id, value.values));
|
|
2569
2677
|
}
|
|
2570
2678
|
else if (value.value?.trim()) {
|
|
2571
|
-
lines.push(`${label}: "${value.value.trim()}"`);
|
|
2679
|
+
lines.push(of(`${label}: "${value.value.trim()}"`));
|
|
2572
2680
|
}
|
|
2573
2681
|
}
|
|
2574
2682
|
const frequency = this._filterState.frequency();
|
|
2575
2683
|
if (frequency) {
|
|
2576
2684
|
const option = FREQUENCY_OPTIONS.find((item) => item.value === frequency);
|
|
2577
|
-
lines.push(`Frequency: ${option?.name ?? frequency}`);
|
|
2685
|
+
lines.push(of(`Frequency: ${option?.name ?? frequency}`));
|
|
2578
2686
|
}
|
|
2579
|
-
return lines;
|
|
2687
|
+
return lines.length ? forkJoin(lines) : of([]);
|
|
2688
|
+
}
|
|
2689
|
+
// One select group's line, with its stored ids resolved to the same names the
|
|
2690
|
+
// on-screen chips show — a cover reading "Organization: 8774, 8777" tells the
|
|
2691
|
+
// client nothing about what the report covers.
|
|
2692
|
+
//
|
|
2693
|
+
// Uses the report bar's own loader and resolver so the two can't drift, which
|
|
2694
|
+
// also means a value whose entity was deleted keeps showing its raw id rather
|
|
2695
|
+
// than dropping off the cover and silently understating the filter. A failed
|
|
2696
|
+
// option fetch degrades the whole line to ids for the same reason: an export
|
|
2697
|
+
// must never fail on a cosmetic lookup.
|
|
2698
|
+
_selectLine(label, group, reportId, values) {
|
|
2699
|
+
return resolveOptionValues(loadGroupOptions(group, this._reportData, reportId), values)
|
|
2700
|
+
.pipe(map$1((resolved) => `${label}: ${resolved.map((item) => item.name).join(', ')}`), catchError(() => of(`${label}: ${values.map((item) => String(item)).join(', ')}`)));
|
|
2580
2701
|
}
|
|
2581
2702
|
_date(value) {
|
|
2582
2703
|
if (!value) {
|
|
@@ -2599,7 +2720,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2599
2720
|
// complete synchronously after setOption.
|
|
2600
2721
|
async function renderChartImage(component, data, width, height, pixelRatio = 3) {
|
|
2601
2722
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
2602
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
2723
|
+
const echarts = (await import('./firestitch-report-echarts-D4VLD05J.mjs')).default;
|
|
2603
2724
|
const host = document.createElement('div');
|
|
2604
2725
|
host.style.width = `${width}px`;
|
|
2605
2726
|
host.style.height = `${height}px`;
|
|
@@ -2738,7 +2859,7 @@ class ReportPdfService {
|
|
|
2738
2859
|
const config = component.config;
|
|
2739
2860
|
const row = data.rows[0] ?? {};
|
|
2740
2861
|
const column = config.measure?.column ?? Object.keys(row)[0];
|
|
2741
|
-
const value = formatMeasureValue(row[column], config.measure?.format);
|
|
2862
|
+
const value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
|
|
2742
2863
|
doc.setFont('helvetica', 'bold');
|
|
2743
2864
|
doc.setFontSize(26);
|
|
2744
2865
|
doc.setTextColor('#1f2933');
|
|
@@ -2968,7 +3089,7 @@ class ReportPptxService {
|
|
|
2968
3089
|
const config = component.config;
|
|
2969
3090
|
const row = data.rows[0] ?? {};
|
|
2970
3091
|
const column = config.measure?.column ?? Object.keys(row)[0];
|
|
2971
|
-
const value = formatMeasureValue(row[column], config.measure?.format);
|
|
3092
|
+
const value = formatMeasureValue(row[column], config.measure?.format, config.measure?.precision);
|
|
2972
3093
|
slide.addText([
|
|
2973
3094
|
{ text: value, options: { fontSize: 28, bold: true, color: '1F2933', breakLine: true } },
|
|
2974
3095
|
{ text: config.measure?.label ?? '', options: { fontSize: 10, color: '7B8794' } },
|
|
@@ -3073,6 +3194,9 @@ class ReportComponent {
|
|
|
3073
3194
|
reportFilterConfig;
|
|
3074
3195
|
_split;
|
|
3075
3196
|
_chatPanel;
|
|
3197
|
+
// Only the canvas knows which page is on screen, and a page-scoped export
|
|
3198
|
+
// has to ask it.
|
|
3199
|
+
_canvas;
|
|
3076
3200
|
introMessage = {
|
|
3077
3201
|
text: '',
|
|
3078
3202
|
};
|
|
@@ -3205,6 +3329,19 @@ class ReportComponent {
|
|
|
3205
3329
|
this._process.run('Exporting PDF', this._exportCollector.collect(report)
|
|
3206
3330
|
.pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
|
|
3207
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
|
+
}
|
|
3208
3345
|
// React to each chat turn (fs-chat handles the request + persistence); we
|
|
3209
3346
|
// only care about the report side effects this context's backend flags.
|
|
3210
3347
|
onChatResponse(response) {
|
|
@@ -3345,8 +3482,8 @@ class ReportComponent {
|
|
|
3345
3482
|
ReportPdfService,
|
|
3346
3483
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
3347
3484
|
// this route's chunk.
|
|
3348
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
3349
|
-
], 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"], outputs: ["response"] }, { 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 });
|
|
3350
3487
|
}
|
|
3351
3488
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
|
|
3352
3489
|
type: Component,
|
|
@@ -3359,7 +3496,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3359
3496
|
ReportPdfService,
|
|
3360
3497
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
3361
3498
|
// this route's chunk.
|
|
3362
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
3499
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
|
|
3363
3500
|
], imports: [
|
|
3364
3501
|
FormsModule,
|
|
3365
3502
|
FsAutocompleteChipsModule,
|
|
@@ -3368,13 +3505,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3368
3505
|
FsAiChatComponent,
|
|
3369
3506
|
MatIcon,
|
|
3370
3507
|
ReportCanvasComponent,
|
|
3371
|
-
], 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"] }]
|
|
3372
3509
|
}], ctorParameters: () => [], propDecorators: { _split: [{
|
|
3373
3510
|
type: ViewChild,
|
|
3374
3511
|
args: ['split', { static: true }]
|
|
3375
3512
|
}], _chatPanel: [{
|
|
3376
3513
|
type: ViewChild,
|
|
3377
3514
|
args: ['chatPanel', { static: true, read: ElementRef }]
|
|
3515
|
+
}], _canvas: [{
|
|
3516
|
+
type: ViewChild,
|
|
3517
|
+
args: [ReportCanvasComponent]
|
|
3378
3518
|
}] } });
|
|
3379
3519
|
|
|
3380
3520
|
// The AI Knowledge document's version history (append-only), each version
|
|
@@ -3567,6 +3707,10 @@ class SemanticDatabaseComponent {
|
|
|
3567
3707
|
K: 'Stopped',
|
|
3568
3708
|
F: 'Failed',
|
|
3569
3709
|
};
|
|
3710
|
+
// The streamed frames that carry a line for the feed: progress, and the final
|
|
3711
|
+
// frame the backend sends when a run ends badly. Without the error kind the
|
|
3712
|
+
// feed stops on the last progress line and never says what went wrong.
|
|
3713
|
+
static LOG_KINDS = ['log', 'error'];
|
|
3570
3714
|
// A row that is still owed content says so; a built one says nothing. On a
|
|
3571
3715
|
// finished build every stage is done on every row, so chipping them all was
|
|
3572
3716
|
// noise that buried the one row that mattered.
|
|
@@ -3706,7 +3850,7 @@ class SemanticDatabaseComponent {
|
|
|
3706
3850
|
// A hard stream/transport failure (the build couldn't even start)
|
|
3707
3851
|
// finalizes here — completed$ won't fire in that case.
|
|
3708
3852
|
tap({ error: (error) => this._finalizeBuild(error) }), filter((event) => event?.type === StreamEventType.Data
|
|
3709
|
-
&& event.data?.kind
|
|
3853
|
+
&& SemanticDatabaseComponent.LOG_KINDS.includes(event.data?.kind)
|
|
3710
3854
|
&& typeof event.data.line === 'string'), map((event) => event.data.line));
|
|
3711
3855
|
this._process
|
|
3712
3856
|
.run('Semantic database build', target$)
|
|
@@ -3718,9 +3862,10 @@ class SemanticDatabaseComponent {
|
|
|
3718
3862
|
* Wrap up one build run exactly once. On a hard stream error, surface the
|
|
3719
3863
|
* actual server-side reason pulled off the broken stream — the generic
|
|
3720
3864
|
* "could not run" line only when the failure carried no server text at all;
|
|
3721
|
-
* otherwise refresh the status and announce how the build actually ended
|
|
3722
|
-
* build
|
|
3723
|
-
*
|
|
3865
|
+
* otherwise refresh the status and announce how the build actually ended. A
|
|
3866
|
+
* failed build always names its reason on the row (the backend records one for
|
|
3867
|
+
* every way a run can die, fatals included), and that reason must surface
|
|
3868
|
+
* instead of the run silently doing nothing.
|
|
3724
3869
|
*/
|
|
3725
3870
|
_finalizeBuild(streamError) {
|
|
3726
3871
|
if (this._buildFinalized) {
|
|
@@ -3732,7 +3877,7 @@ class SemanticDatabaseComponent {
|
|
|
3732
3877
|
const detail = this._streamErrorDetail(streamError);
|
|
3733
3878
|
this._message.error(detail
|
|
3734
3879
|
? `The semantic database build failed — ${detail}`
|
|
3735
|
-
: 'The semantic database build could not run —
|
|
3880
|
+
: 'The semantic database build could not run — the connection to the server was lost.');
|
|
3736
3881
|
this._refreshStatus();
|
|
3737
3882
|
return;
|
|
3738
3883
|
}
|
|
@@ -3996,18 +4141,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3996
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"] }]
|
|
3997
4142
|
}], ctorParameters: () => [] });
|
|
3998
4143
|
|
|
3999
|
-
// The reports listing — an fs-
|
|
4000
|
-
// pattern (a ':id' token → the report id; a leading
|
|
4001
|
-
// otherwise it's relative to this component's route).
|
|
4002
|
-
// title, creates the report, then routes to it. The
|
|
4003
|
-
// global Report Settings dialog (AI knowledge document +
|
|
4004
|
-
// 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".
|
|
4005
4156
|
//
|
|
4006
4157
|
// Reports are soft-deletable: the row kebab deletes, "Show Deleted" flips the
|
|
4007
|
-
//
|
|
4008
|
-
// that mode
|
|
4009
|
-
//
|
|
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.
|
|
4010
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;
|
|
4011
4168
|
// API base for the reports endpoints; override to mount elsewhere.
|
|
4012
4169
|
basePath = input('reports');
|
|
4013
4170
|
// Where a row links to; ':id' is replaced with the report id. Default takes
|
|
@@ -4015,27 +4172,44 @@ class FsAiReportsComponent {
|
|
|
4015
4172
|
openPattern = input(':id');
|
|
4016
4173
|
// Where a freshly-created report routes to; same ':id' pattern rules.
|
|
4017
4174
|
createPattern = input(':id');
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
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);
|
|
4023
4182
|
_reportData = inject(ReportData);
|
|
4024
4183
|
_router = inject(Router);
|
|
4025
4184
|
_route = inject(ActivatedRoute);
|
|
4026
4185
|
_prompt = inject(FsPrompt);
|
|
4186
|
+
_message = inject(FsMessage);
|
|
4027
4187
|
_dialog = inject(MatDialog);
|
|
4028
4188
|
_destroyRef = inject(DestroyRef);
|
|
4189
|
+
// The filter bar's current query — carries `state` when Show Deleted is on.
|
|
4190
|
+
_query = {};
|
|
4029
4191
|
constructor() {
|
|
4030
4192
|
effect(() => {
|
|
4031
4193
|
this._reportData.basePath = this.basePath();
|
|
4032
4194
|
});
|
|
4033
|
-
|
|
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();
|
|
4034
4203
|
}
|
|
4035
4204
|
// routerLink commands for a report row (built from openPattern).
|
|
4036
4205
|
openLink(report) {
|
|
4037
4206
|
return this._patternCommands(this.openPattern(), report.id);
|
|
4038
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
|
+
}
|
|
4039
4213
|
// Prompt for a title, create the report, then route to it (createPattern).
|
|
4040
4214
|
createReport() {
|
|
4041
4215
|
this._prompt.input({
|
|
@@ -4044,27 +4218,12 @@ class FsAiReportsComponent {
|
|
|
4044
4218
|
commitLabel: 'Create',
|
|
4045
4219
|
required: true,
|
|
4046
4220
|
})
|
|
4047
|
-
.pipe(switchMap((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
|
|
4221
|
+
.pipe(switchMap$1((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
|
|
4048
4222
|
.subscribe((report) => {
|
|
4049
4223
|
const pattern = this.createPattern();
|
|
4050
4224
|
this._router.navigate(this._patternCommands(pattern, report.id), pattern.startsWith('/') ? {} : { relativeTo: this._route });
|
|
4051
4225
|
});
|
|
4052
4226
|
}
|
|
4053
|
-
// Turn a ':id' pattern into router commands. Relative unless it starts
|
|
4054
|
-
// with '/', in which case the first segment carries the leading slash.
|
|
4055
|
-
_patternCommands(pattern, id) {
|
|
4056
|
-
const absolute = pattern.startsWith('/');
|
|
4057
|
-
const segments = pattern.split('/')
|
|
4058
|
-
.filter((segment) => segment.length > 0)
|
|
4059
|
-
.map((segment) => (segment === ':id' ? id : segment));
|
|
4060
|
-
if (!segments.length) {
|
|
4061
|
-
return [id];
|
|
4062
|
-
}
|
|
4063
|
-
if (absolute) {
|
|
4064
|
-
segments[0] = `/${segments[0]}`;
|
|
4065
|
-
}
|
|
4066
|
-
return segments;
|
|
4067
|
-
}
|
|
4068
4227
|
// The global Report Settings dialog: the AI knowledge document and the
|
|
4069
4228
|
// semantic-database build. A running build streams into the FsProcess dock,
|
|
4070
4229
|
// which survives the dialog closing.
|
|
@@ -4076,66 +4235,315 @@ class FsAiReportsComponent {
|
|
|
4076
4235
|
autoFocus: false,
|
|
4077
4236
|
});
|
|
4078
4237
|
}
|
|
4079
|
-
|
|
4080
|
-
this.
|
|
4081
|
-
|
|
4082
|
-
|
|
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
|
+
],
|
|
4083
4255
|
actions: [
|
|
4084
4256
|
{
|
|
4085
|
-
|
|
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
|
+
{
|
|
4086
4269
|
icon: 'settings',
|
|
4087
4270
|
tooltip: 'Report settings',
|
|
4088
4271
|
primary: false,
|
|
4089
4272
|
click: () => this.openSettings(),
|
|
4090
4273
|
},
|
|
4274
|
+
{
|
|
4275
|
+
label: 'New Folder',
|
|
4276
|
+
primary: false,
|
|
4277
|
+
click: () => this._createFolder(null),
|
|
4278
|
+
},
|
|
4091
4279
|
{
|
|
4092
4280
|
label: 'Create',
|
|
4093
4281
|
primary: true,
|
|
4094
4282
|
click: () => this.createReport(),
|
|
4095
4283
|
},
|
|
4096
4284
|
],
|
|
4097
|
-
|
|
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: [
|
|
4098
4329
|
{
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
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
|
+
],
|
|
4106
4356
|
},
|
|
4107
4357
|
],
|
|
4108
|
-
// `filter: true` makes fs-list render the Show Deleted toggle and send
|
|
4109
|
-
// `query` with the fetch; in that mode it also swaps the row's Delete
|
|
4110
|
-
// action for Restore.
|
|
4111
|
-
restore: {
|
|
4112
|
-
query: { state: ReportState.Deleted },
|
|
4113
|
-
filter: true,
|
|
4114
|
-
filterLabel: 'Show Deleted',
|
|
4115
|
-
menuLabel: 'Restore',
|
|
4116
|
-
reload: true,
|
|
4117
|
-
click: (report) => this._reportData.undelete(report.id),
|
|
4118
|
-
},
|
|
4119
|
-
fetch: (query) => {
|
|
4120
|
-
return this._reportData.reports(query)
|
|
4121
|
-
.pipe(map((reports) => ({ data: reports ?? [] })));
|
|
4122
|
-
},
|
|
4123
4358
|
};
|
|
4124
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
|
+
}
|
|
4125
4534
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4126
|
-
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 });
|
|
4127
4536
|
}
|
|
4128
4537
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
|
|
4129
4538
|
type: Component,
|
|
4130
4539
|
args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
|
|
4131
|
-
|
|
4132
|
-
|
|
4540
|
+
FsFilterModule,
|
|
4541
|
+
FsSkeletonModule,
|
|
4542
|
+
FsTreeModule,
|
|
4543
|
+
MatIconModule,
|
|
4133
4544
|
RouterLink,
|
|
4134
|
-
], template: "<fs-
|
|
4135
|
-
}], ctorParameters: () => []
|
|
4136
|
-
type: ViewChild,
|
|
4137
|
-
args: [FsListComponent]
|
|
4138
|
-
}] } });
|
|
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: () => [] });
|
|
4139
4547
|
|
|
4140
4548
|
// fs-filter query key for the runtime Frequency control (a render-layer bucket
|
|
4141
4549
|
// override, not a filter group — so it's read straight off the query).
|
|
@@ -4161,6 +4569,9 @@ class FsAiReportComponent {
|
|
|
4161
4569
|
reportFilterConfig;
|
|
4162
4570
|
_split;
|
|
4163
4571
|
_chatPanel;
|
|
4572
|
+
// Only the canvas knows which page is on screen, and a page-scoped export
|
|
4573
|
+
// has to ask it.
|
|
4574
|
+
_canvas;
|
|
4164
4575
|
_reportData = inject(ReportData);
|
|
4165
4576
|
_filterState = inject(ReportFilterStateService);
|
|
4166
4577
|
_exportCollector = inject(ReportExportCollectorService);
|
|
@@ -4264,6 +4675,19 @@ class FsAiReportComponent {
|
|
|
4264
4675
|
this._process.run('Exporting PDF', this._exportCollector.collect(report)
|
|
4265
4676
|
.pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
|
|
4266
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
|
+
}
|
|
4267
4691
|
// React to each chat turn (fs-chat handles the request + persistence); we
|
|
4268
4692
|
// only care about the report side effects this context's backend flags.
|
|
4269
4693
|
onChatResponse(response) {
|
|
@@ -4389,8 +4813,8 @@ class FsAiReportComponent {
|
|
|
4389
4813
|
ReportPptxService,
|
|
4390
4814
|
ReportPdfService,
|
|
4391
4815
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
4392
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
4393
|
-
], 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"], outputs: ["response"] }, { 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 });
|
|
4394
4818
|
}
|
|
4395
4819
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
|
|
4396
4820
|
type: Component,
|
|
@@ -4402,7 +4826,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4402
4826
|
ReportPptxService,
|
|
4403
4827
|
ReportPdfService,
|
|
4404
4828
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
4405
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
4829
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-D4VLD05J.mjs').then((module) => module.default) }),
|
|
4406
4830
|
], imports: [
|
|
4407
4831
|
FsFilterModule,
|
|
4408
4832
|
FsMenuModule,
|
|
@@ -4411,13 +4835,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4411
4835
|
MatIconModule,
|
|
4412
4836
|
MatTooltipModule,
|
|
4413
4837
|
ReportCanvasComponent,
|
|
4414
|
-
], 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"] }]
|
|
4415
4839
|
}], ctorParameters: () => [], propDecorators: { _split: [{
|
|
4416
4840
|
type: ViewChild,
|
|
4417
4841
|
args: ['split', { static: true }]
|
|
4418
4842
|
}], _chatPanel: [{
|
|
4419
4843
|
type: ViewChild,
|
|
4420
4844
|
args: ['chatPanel', { static: true, read: ElementRef }]
|
|
4845
|
+
}], _canvas: [{
|
|
4846
|
+
type: ViewChild,
|
|
4847
|
+
args: [ReportCanvasComponent]
|
|
4421
4848
|
}] } });
|
|
4422
4849
|
|
|
4423
4850
|
/*
|
|
@@ -4432,5 +4859,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4432
4859
|
* Generated bundle index. Do not edit.
|
|
4433
4860
|
*/
|
|
4434
4861
|
|
|
4435
|
-
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,
|
|
4436
|
-
//# 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
|