@firestitch/report 18.0.28 → 18.0.29
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/components/report-canvas/report-canvas.component.d.ts +3 -1
- package/app/reports/components/report-component/report-component.component.d.ts +2 -1
- package/app/reports/data/report.data.d.ts +1 -1
- package/app/reports/dialogs/component-settings/component-settings.component.d.ts +6 -4
- package/app/reports/export/report-export-collector.service.d.ts +2 -0
- package/app/reports/export/report-export.service.d.ts +15 -0
- package/app/reports/interfaces/report.interface.d.ts +4 -0
- package/app/reports/views/report/fs-ai-report.component.d.ts +1 -3
- package/app/reports/views/report/report.component.d.ts +1 -3
- package/esm2022/app/reports/components/report-canvas/report-canvas.component.mjs +17 -3
- package/esm2022/app/reports/components/report-component/report-component.component.mjs +21 -5
- package/esm2022/app/reports/data/report.data.mjs +8 -3
- package/esm2022/app/reports/dialogs/component-settings/component-settings.component.mjs +10 -11
- package/esm2022/app/reports/export/report-export-collector.service.mjs +59 -11
- package/esm2022/app/reports/export/report-export.service.mjs +77 -0
- package/esm2022/app/reports/export/report-pdf.service.mjs +3 -2
- package/esm2022/app/reports/export/report-pptx.service.mjs +3 -2
- package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
- package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +32 -20
- package/esm2022/app/reports/views/report/report.component.mjs +33 -21
- package/fesm2022/{firestitch-report-echarts-CTReuXLz.mjs → firestitch-report-echarts-DeE74U3a.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-CTReuXLz.mjs.map → firestitch-report-echarts-DeE74U3a.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-DdFptz2k.mjs → firestitch-report-firestitch-report-Nxl-C1w0.mjs} +245 -70
- package/fesm2022/firestitch-report-firestitch-report-Nxl-C1w0.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/firestitch-report-firestitch-report-DdFptz2k.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, Component, ChangeDetectionStrategy, Input, DestroyRef, ViewChild, EventEmitter, ChangeDetectorRef, NgZone, ElementRef, Output, HostBinding, signal, CUSTOM_ELEMENTS_SCHEMA, model, input, computed, viewChild, effect, output } from '@angular/core';
|
|
2
|
+
import { inject, Injectable, Component, ChangeDetectionStrategy, Input, DestroyRef, ViewChild, EventEmitter, ChangeDetectorRef, NgZone, ElementRef, Output, HostBinding, ViewChildren, signal, CUSTOM_ELEMENTS_SCHEMA, model, input, computed, viewChild, effect, output } from '@angular/core';
|
|
3
3
|
import * as i1$3 from '@angular/forms';
|
|
4
4
|
import { FormsModule, ControlContainer, NgForm } from '@angular/forms';
|
|
5
5
|
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialog, MatDialogClose } from '@angular/material/dialog';
|
|
@@ -13,8 +13,7 @@ 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 {
|
|
17
|
-
import { map, shareReplay, filter, tap, switchMap, catchError, finalize, take } from 'rxjs/operators';
|
|
16
|
+
import { map, shareReplay, filter, tap, switchMap, mergeMap, toArray, catchError, finalize, takeUntil, take } from 'rxjs/operators';
|
|
18
17
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
19
18
|
import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
|
|
20
19
|
import { FsAiChatComponent } from '@firestitch/ai';
|
|
@@ -27,6 +26,7 @@ import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
|
|
|
27
26
|
import { FsApi, StreamEventType } from '@firestitch/api';
|
|
28
27
|
import * as i1$1 from '@angular/material/menu';
|
|
29
28
|
import { MatMenuModule } from '@angular/material/menu';
|
|
29
|
+
import { of, Subject, map as map$1, debounceTime, forkJoin, from, EMPTY, BehaviorSubject, switchMap as switchMap$1 } from 'rxjs';
|
|
30
30
|
import { format, parseISO, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek, isValid } from 'date-fns';
|
|
31
31
|
import { DatePipe } from '@angular/common';
|
|
32
32
|
import * as i1 from '@firestitch/list';
|
|
@@ -190,8 +190,13 @@ class ReportData {
|
|
|
190
190
|
}
|
|
191
191
|
// One component's data. The body carries everything session-scoped: the
|
|
192
192
|
// resolved filter values and the component's own interaction state.
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
//
|
|
194
|
+
// `isExport` marks the read as part of a PDF/PowerPoint render. The server
|
|
195
|
+
// gives those a longer per-statement ceiling than an on-screen tile, because
|
|
196
|
+
// a reader watching an export progress bar can wait for a query nobody would
|
|
197
|
+
// tolerate waiting for on a dashboard.
|
|
198
|
+
componentData(reportId, componentId, filters = [], state = {}, isExport = false, config = {}) {
|
|
199
|
+
return this._api.post(this._path(`${reportId}/components/${componentId}/data`), { filters, state, export: isExport }, {
|
|
195
200
|
key: 'data',
|
|
196
201
|
...config,
|
|
197
202
|
});
|
|
@@ -1583,11 +1588,16 @@ class ReportComponentComponent {
|
|
|
1583
1588
|
this._buildFilterConfig();
|
|
1584
1589
|
}
|
|
1585
1590
|
}
|
|
1586
|
-
// fs-filter reads its config only once at init, so the header is keyed on
|
|
1587
|
-
// set of component-level filter groups
|
|
1588
|
-
//
|
|
1591
|
+
// fs-filter reads its config only once at init, so the header is keyed on what
|
|
1592
|
+
// it renders: the set of component-level filter groups AND the title, which
|
|
1593
|
+
// lives in fs-filter's heading template. fs-filter is OnPush and the heading is
|
|
1594
|
+
// rendered inside ITS view, so marking this component does not repaint the
|
|
1595
|
+
// title — recreating the block does.
|
|
1589
1596
|
get filterKey() {
|
|
1590
|
-
return
|
|
1597
|
+
return [
|
|
1598
|
+
this.component.title,
|
|
1599
|
+
...this.componentFilterGroups.map((group) => `${group.id}:${group.level}`),
|
|
1600
|
+
].join('|');
|
|
1591
1601
|
}
|
|
1592
1602
|
get truncated() {
|
|
1593
1603
|
return !!this.data?.truncated;
|
|
@@ -1623,6 +1633,17 @@ class ReportComponentComponent {
|
|
|
1623
1633
|
settings() {
|
|
1624
1634
|
this.openSettings.emit(this.component);
|
|
1625
1635
|
}
|
|
1636
|
+
// Presentation settings saved from the settings dialog. Title and padding are
|
|
1637
|
+
// not part of the query, so the data already fetched still stands — this only
|
|
1638
|
+
// repaints. The header config is rebuilt because `filterKey` changes with the
|
|
1639
|
+
// title and recreates the fs-filter block around it. The chart follows its box
|
|
1640
|
+
// on its own (ngx-echarts observes the host), so nothing is told to resize.
|
|
1641
|
+
applySettings(settings) {
|
|
1642
|
+
this.component.title = settings.title;
|
|
1643
|
+
this.component.config = { ...this.component.config, padding: settings.padding };
|
|
1644
|
+
this._buildFilterConfig();
|
|
1645
|
+
this._cdRef.markForCheck();
|
|
1646
|
+
}
|
|
1626
1647
|
exportCsv() {
|
|
1627
1648
|
const filters = this._filterState.resolveForComponent(this.component);
|
|
1628
1649
|
this._process.download(`Exporting ${this.component.title ?? 'component'}`, this._reportData.exportComponent(this.reportId, this.component.id, filters));
|
|
@@ -1952,6 +1973,9 @@ class ReportCanvasComponent {
|
|
|
1952
1973
|
// editMode flag, so it flips it off (mirrors the menu's "Done editing layout").
|
|
1953
1974
|
editDone = new EventEmitter();
|
|
1954
1975
|
zoomPan;
|
|
1976
|
+
// The rendered components of the active page — only the canvas can reach the
|
|
1977
|
+
// one instance a settings save applies to.
|
|
1978
|
+
_renderedComponents;
|
|
1955
1979
|
_viewport;
|
|
1956
1980
|
_guides;
|
|
1957
1981
|
PX = PX_PER_INCH;
|
|
@@ -2022,6 +2046,14 @@ class ReportCanvasComponent {
|
|
|
2022
2046
|
pageLabel(page, index) {
|
|
2023
2047
|
return page.name || `Page ${index + 1}`;
|
|
2024
2048
|
}
|
|
2049
|
+
// Hand presentation settings (title, padding) saved in the settings dialog to
|
|
2050
|
+
// the one component they belong to. Nothing here feeds the query, so that
|
|
2051
|
+
// component repaints and every other one on the page is left alone.
|
|
2052
|
+
applyComponentSettings(componentId, settings) {
|
|
2053
|
+
this._renderedComponents
|
|
2054
|
+
.find((rendered) => rendered.component.id === componentId)
|
|
2055
|
+
?.applySettings(settings);
|
|
2056
|
+
}
|
|
2025
2057
|
selectComponent(componentId) {
|
|
2026
2058
|
this.selectedComponentId = componentId;
|
|
2027
2059
|
this._cdRef.markForCheck();
|
|
@@ -2231,7 +2263,7 @@ class ReportCanvasComponent {
|
|
|
2231
2263
|
this._guides?.nativeElement?.replaceChildren();
|
|
2232
2264
|
}
|
|
2233
2265
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2234
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportCanvasComponent, isStandalone: true, selector: "app-report-canvas", inputs: { report: "report", editMode: "editMode" }, outputs: { componentSettings: "componentSettings", reportChanged: "reportChanged", editDone: "editDone" }, viewQueries: [{ propertyName: "zoomPan", first: true, predicate: FsZoomPanComponent, descendants: true }, { propertyName: "_viewport", first: true, predicate: ["viewport"], descendants: true, static: true }, { propertyName: "_guides", first: true, predicate: ["guides"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage; as page) {\n <div\n class=\"page\"\n [class.flow]=\"isFlow\"\n [class.editing]=\"editMode\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.padding.px]=\"isFlow ? flowPadding * PX : 0\"\n [style.--canvas-zoom]=\"zoom\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n @if (isFlow) {\n @for (component of flowComponents; track component.id) {\n <app-report-component\n class=\"flow-item\"\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"editMode\"\n [layout]=\"'flow'\"\n [zoom]=\"zoom\"\n [snapper]=\"snapper\"\n [selected]=\"component.id === selectedComponentId\"\n [style.width.%]=\"component.flowWidth\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (flowReorder)=\"onFlowReorder($event)\"\n (openSettings)=\"componentSettings.emit($event)\">\n </app-report-component>\n }\n } @else {\n @for (component of page.components; track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"editMode\"\n [layout]=\"'freeform'\"\n [zoom]=\"zoom\"\n [snapper]=\"snapper\"\n [selected]=\"component.id === selectedComponentId\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (openSettings)=\"componentSettings.emit($event)\">\n </app-report-component>\n }\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!page.components.length) {\n <div class=\"page-empty\">\n Ask the assistant to add a chart, list or KPI to this page.\n </div>\n }\n </div>\n }\n </fs-zoom-pan>\n\n @if (editMode) {\n <button\n mat-flat-button\n type=\"button\"\n color=\"primary\"\n class=\"edit-done\"\n matTooltip=\"Exit layout editing\"\n (click)=\"editDone.emit()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n }\n </div>\n\n <div class=\"canvas-toolbar\">\n <div class=\"page-tabs\">\n @if ((report?.pages?.length ?? 0) > 1) {\n @for (page of report.pages; track page.id; let index = $index) {\n <button\n type=\"button\"\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex\"\n (click)=\"selectPage(index)\">\n {{ pageLabel(page, index) }}\n </button>\n }\n }\n <button\n type=\"button\"\n class=\"page-tab add-page\"\n matTooltip=\"Add page\"\n (click)=\"addPage()\">\n <mat-icon>add</mat-icon>\n Page\n </button>\n </div>\n\n <div class=\"zoom-controls\">\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom out\"\n (click)=\"zoomOut()\">\n <mat-icon>remove</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-label\"\n matTooltip=\"Zoom to 100%\"\n (click)=\"zoomActual()\">\n {{ (zoom * 100).toFixed(0) }}%\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom in\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-width:0;min-height:0;flex:1;margin-top:10px}.canvas{display:flex;flex-direction:column;gap:8px;flex:1;min-height:0}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px}.canvas-toolbar .page-tabs{display:flex;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs .page-tab{border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 14px;font-size:12px;color:#52606d;cursor:pointer}.canvas-toolbar .page-tabs .page-tab.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.canvas-toolbar .page-tabs .page-tab.add-page{display:inline-flex;align-items:center;gap:2px;padding-left:8px;border-style:dashed;color:#52606d}.canvas-toolbar .page-tabs .page-tab.add-page mat-icon{font-size:16px;width:16px;height:16px}.canvas-toolbar .zoom-controls{display:flex;align-items:center;gap:2px;margin-left:auto;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .zoom-controls .zoom-button{display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-button:hover{background:#f0f4f8}.canvas-toolbar .zoom-controls .zoom-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .zoom-controls .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-label:hover{color:#1f2933}.page-viewport{position:relative;flex:1;min-height:420px;border:1px solid #e4e7eb;border-radius:8px;background:#eef1f5;overflow:hidden}.page-viewport fs-zoom-pan{width:100%;height:100%}.edit-done{position:absolute;top:12px;right:12px;z-index:40;border-radius:999px;box-shadow:0 2px 10px #0f172a2e}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.flow{display:flex;flex-wrap:wrap;align-content:flex-start;align-items:flex-start}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.guides{position:absolute;inset:0;pointer-events:none;z-index:30}.guides ::ng-deep .guide{position:absolute;background:#2196f3}.guides ::ng-deep .guide.guide-v{top:0;bottom:0;width:calc(1.25px / var(--canvas-zoom, 1))}.guides ::ng-deep .guide.guide-h{left:0;right:0;height:calc(1.25px / var(--canvas-zoom, 1))}.page-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;font-size:14px;pointer-events:none}\n"], dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsZoomPanModule }, { kind: "component", type: i1$2.FsZoomPanComponent, selector: "fs-zoom-pan", inputs: ["zoomMax", "zoomMin", "zoomScale", "zoomDefault", "zoomFactor", "top", "left"], outputs: ["moved", "zoomed"] }, { kind: "component", type: ReportComponentComponent, selector: "app-report-component", inputs: ["reportId", "component", "groups", "editMode", "layout", "zoom", "snapper", "selected"], outputs: ["selectComponent", "positionChanged", "flowReorder", "openSettings"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2266
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportCanvasComponent, isStandalone: true, selector: "app-report-canvas", inputs: { report: "report", editMode: "editMode" }, outputs: { componentSettings: "componentSettings", reportChanged: "reportChanged", editDone: "editDone" }, viewQueries: [{ propertyName: "zoomPan", first: true, predicate: FsZoomPanComponent, descendants: true }, { propertyName: "_viewport", first: true, predicate: ["viewport"], descendants: true, static: true }, { propertyName: "_guides", first: true, predicate: ["guides"], descendants: true }, { propertyName: "_renderedComponents", predicate: ReportComponentComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage; as page) {\n <div\n class=\"page\"\n [class.flow]=\"isFlow\"\n [class.editing]=\"editMode\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.padding.px]=\"isFlow ? flowPadding * PX : 0\"\n [style.--canvas-zoom]=\"zoom\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n @if (isFlow) {\n @for (component of flowComponents; track component.id) {\n <app-report-component\n class=\"flow-item\"\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"editMode\"\n [layout]=\"'flow'\"\n [zoom]=\"zoom\"\n [snapper]=\"snapper\"\n [selected]=\"component.id === selectedComponentId\"\n [style.width.%]=\"component.flowWidth\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (flowReorder)=\"onFlowReorder($event)\"\n (openSettings)=\"componentSettings.emit($event)\">\n </app-report-component>\n }\n } @else {\n @for (component of page.components; track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"editMode\"\n [layout]=\"'freeform'\"\n [zoom]=\"zoom\"\n [snapper]=\"snapper\"\n [selected]=\"component.id === selectedComponentId\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (openSettings)=\"componentSettings.emit($event)\">\n </app-report-component>\n }\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!page.components.length) {\n <div class=\"page-empty\">\n Ask the assistant to add a chart, list or KPI to this page.\n </div>\n }\n </div>\n }\n </fs-zoom-pan>\n\n @if (editMode) {\n <button\n mat-flat-button\n type=\"button\"\n color=\"primary\"\n class=\"edit-done\"\n matTooltip=\"Exit layout editing\"\n (click)=\"editDone.emit()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n }\n </div>\n\n <div class=\"canvas-toolbar\">\n <div class=\"page-tabs\">\n @if ((report?.pages?.length ?? 0) > 1) {\n @for (page of report.pages; track page.id; let index = $index) {\n <button\n type=\"button\"\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex\"\n (click)=\"selectPage(index)\">\n {{ pageLabel(page, index) }}\n </button>\n }\n }\n <button\n type=\"button\"\n class=\"page-tab add-page\"\n matTooltip=\"Add page\"\n (click)=\"addPage()\">\n <mat-icon>add</mat-icon>\n Page\n </button>\n </div>\n\n <div class=\"zoom-controls\">\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom out\"\n (click)=\"zoomOut()\">\n <mat-icon>remove</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-label\"\n matTooltip=\"Zoom to 100%\"\n (click)=\"zoomActual()\">\n {{ (zoom * 100).toFixed(0) }}%\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom in\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-width:0;min-height:0;flex:1;margin-top:10px}.canvas{display:flex;flex-direction:column;gap:8px;flex:1;min-height:0}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px}.canvas-toolbar .page-tabs{display:flex;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs .page-tab{border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 14px;font-size:12px;color:#52606d;cursor:pointer}.canvas-toolbar .page-tabs .page-tab.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.canvas-toolbar .page-tabs .page-tab.add-page{display:inline-flex;align-items:center;gap:2px;padding-left:8px;border-style:dashed;color:#52606d}.canvas-toolbar .page-tabs .page-tab.add-page mat-icon{font-size:16px;width:16px;height:16px}.canvas-toolbar .zoom-controls{display:flex;align-items:center;gap:2px;margin-left:auto;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .zoom-controls .zoom-button{display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-button:hover{background:#f0f4f8}.canvas-toolbar .zoom-controls .zoom-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .zoom-controls .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-label:hover{color:#1f2933}.page-viewport{position:relative;flex:1;min-height:420px;border:1px solid #e4e7eb;border-radius:8px;background:#eef1f5;overflow:hidden}.page-viewport fs-zoom-pan{width:100%;height:100%}.edit-done{position:absolute;top:12px;right:12px;z-index:40;border-radius:999px;box-shadow:0 2px 10px #0f172a2e}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.flow{display:flex;flex-wrap:wrap;align-content:flex-start;align-items:flex-start}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.guides{position:absolute;inset:0;pointer-events:none;z-index:30}.guides ::ng-deep .guide{position:absolute;background:#2196f3}.guides ::ng-deep .guide.guide-v{top:0;bottom:0;width:calc(1.25px / var(--canvas-zoom, 1))}.guides ::ng-deep .guide.guide-h{left:0;right:0;height:calc(1.25px / var(--canvas-zoom, 1))}.page-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;font-size:14px;pointer-events:none}\n"], dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsZoomPanModule }, { kind: "component", type: i1$2.FsZoomPanComponent, selector: "fs-zoom-pan", inputs: ["zoomMax", "zoomMin", "zoomScale", "zoomDefault", "zoomFactor", "top", "left"], outputs: ["moved", "zoomed"] }, { kind: "component", type: ReportComponentComponent, selector: "app-report-component", inputs: ["reportId", "component", "groups", "editMode", "layout", "zoom", "snapper", "selected"], outputs: ["selectComponent", "positionChanged", "flowReorder", "openSettings"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2235
2267
|
}
|
|
2236
2268
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportCanvasComponent, decorators: [{
|
|
2237
2269
|
type: Component,
|
|
@@ -2255,6 +2287,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2255
2287
|
}], zoomPan: [{
|
|
2256
2288
|
type: ViewChild,
|
|
2257
2289
|
args: [FsZoomPanComponent]
|
|
2290
|
+
}], _renderedComponents: [{
|
|
2291
|
+
type: ViewChildren,
|
|
2292
|
+
args: [ReportComponentComponent]
|
|
2258
2293
|
}], _viewport: [{
|
|
2259
2294
|
type: ViewChild,
|
|
2260
2295
|
args: ['viewport', { static: true }]
|
|
@@ -2273,6 +2308,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2273
2308
|
// immediately and refreshes the in-dialog list.
|
|
2274
2309
|
// SQL / Config — read-only diagnostics.
|
|
2275
2310
|
class ComponentSettingsComponent {
|
|
2311
|
+
// A filter was added, removed or re-levelled, so the report's filter groups
|
|
2312
|
+
// changed. Those mutations persist the moment they are clicked and the dialog
|
|
2313
|
+
// can then be dismissed by a route that carries no result (Escape, backdrop),
|
|
2314
|
+
// so the page is told here rather than through the close result.
|
|
2315
|
+
filtersChanged = new EventEmitter();
|
|
2276
2316
|
component;
|
|
2277
2317
|
report;
|
|
2278
2318
|
selectedTab = 'settings';
|
|
@@ -2297,9 +2337,6 @@ class ComponentSettingsComponent {
|
|
|
2297
2337
|
dateRange: 'Date range',
|
|
2298
2338
|
keyword: 'Search text',
|
|
2299
2339
|
};
|
|
2300
|
-
// True once any filter mutation happened — the parent reloads the report so
|
|
2301
|
-
// the canvas + report bar reflect the change.
|
|
2302
|
-
_changed = false;
|
|
2303
2340
|
_data = inject(MAT_DIALOG_DATA);
|
|
2304
2341
|
_dialogRef = inject(MatDialogRef);
|
|
2305
2342
|
_reportData = inject(ReportData);
|
|
@@ -2318,9 +2355,6 @@ class ComponentSettingsComponent {
|
|
|
2318
2355
|
this._seedFilters(this.report, this.component.id);
|
|
2319
2356
|
this._loadColumns();
|
|
2320
2357
|
}
|
|
2321
|
-
get hasChanges() {
|
|
2322
|
-
return this._changed;
|
|
2323
|
-
}
|
|
2324
2358
|
// Human-friendly name for the inferred interface shown beside each column.
|
|
2325
2359
|
typeLabel(type) {
|
|
2326
2360
|
return this._typeLabels[type] ?? type;
|
|
@@ -2335,10 +2369,10 @@ class ComponentSettingsComponent {
|
|
|
2335
2369
|
left: this.paddingLeft,
|
|
2336
2370
|
},
|
|
2337
2371
|
};
|
|
2338
|
-
return this._reportData.updateComponent(this.report.id, this.component.id, settings)
|
|
2372
|
+
return this._reportData.updateComponent(this.report.id, this.component.id, { ...settings })
|
|
2339
2373
|
.pipe(tap(() => {
|
|
2340
2374
|
this._message.success('Component saved');
|
|
2341
|
-
this._dialogRef.close(
|
|
2375
|
+
this._dialogRef.close({ settings });
|
|
2342
2376
|
}));
|
|
2343
2377
|
};
|
|
2344
2378
|
// Flip one exposure (report bar or this component) for a row. We update the
|
|
@@ -2396,7 +2430,7 @@ class ComponentSettingsComponent {
|
|
|
2396
2430
|
// After any filter mutation: re-fetch the report so the dialog's filter list
|
|
2397
2431
|
// (and the parent canvas, on close) reflect the new groups/levels.
|
|
2398
2432
|
_afterMutation() {
|
|
2399
|
-
this.
|
|
2433
|
+
this.filtersChanged.emit();
|
|
2400
2434
|
this._reportData.get(this.report.id)
|
|
2401
2435
|
.subscribe((report) => {
|
|
2402
2436
|
this.report = report;
|
|
@@ -2748,29 +2782,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2748
2782
|
// Rows fetched for a list component's export table — enough to fill a
|
|
2749
2783
|
// slide/page table; the cap note covers the rest.
|
|
2750
2784
|
const LIST_EXPORT_ROWS = 100;
|
|
2785
|
+
// How many tile queries an export runs at once.
|
|
2786
|
+
//
|
|
2787
|
+
// Not a politeness setting — it is the difference between a deck and a broken
|
|
2788
|
+
// one. These queries are heavy and read the same tables, so firing all of them
|
|
2789
|
+
// together puts every one in contention for the same database at the same
|
|
2790
|
+
// moment and they cross the statement ceiling as a group: measured on report
|
|
2791
|
+
// 28, ten tiles that each answer in 9-24s run one after another all failed at
|
|
2792
|
+
// ~30s when released together. Concurrency buys wall-clock only up to the point
|
|
2793
|
+
// where the queries start taking it back off each other, and past that it buys
|
|
2794
|
+
// failures.
|
|
2795
|
+
const EXPORT_CONCURRENCY = 3;
|
|
2796
|
+
// What a component with nothing to draw prints in the exported file.
|
|
2797
|
+
//
|
|
2798
|
+
// The two cases must not read alike. A tile that returned no rows is a finding
|
|
2799
|
+
// about the period — the report worked and the answer is "none". A tile whose
|
|
2800
|
+
// query failed is a hole in the file, and calling that "Data unavailable" tells
|
|
2801
|
+
// the reader the data does not exist when what happened is that nobody managed
|
|
2802
|
+
// to ask for it. Shared by both renderers so the PDF and the deck cannot end up
|
|
2803
|
+
// describing the same hole two different ways.
|
|
2804
|
+
function emptyLabel(item) {
|
|
2805
|
+
return item.data ? 'No data' : 'Could not be generated';
|
|
2806
|
+
}
|
|
2807
|
+
// The pages of a collected report that have at least one tile whose query
|
|
2808
|
+
// failed — what the export gate names to the user, and what makes "some of this
|
|
2809
|
+
// file is missing" a fact the caller can act on rather than a thing they notice
|
|
2810
|
+
// in the PDF afterwards.
|
|
2811
|
+
function failedPageNames(collected) {
|
|
2812
|
+
// Numbered before the filter — an unnamed page 4 that failed has to read
|
|
2813
|
+
// "Page 4", not "Page 1" because it happened to be the first failure.
|
|
2814
|
+
return collected.pages
|
|
2815
|
+
.map((page, index) => ({ page, number: index + 1 }))
|
|
2816
|
+
.filter(({ page }) => page.components.some((item) => !item.data))
|
|
2817
|
+
.map(({ page, number }) => page.page.name || `Page ${number}`);
|
|
2818
|
+
}
|
|
2751
2819
|
// Gathers everything an export needs: every component's data across ALL pages
|
|
2752
2820
|
// (the canvas only renders the active page, so exports can't scrape the
|
|
2753
|
-
// screen), fetched fresh with the viewer's current filter state.
|
|
2754
|
-
//
|
|
2821
|
+
// screen), fetched fresh with the viewer's current filter state.
|
|
2822
|
+
//
|
|
2823
|
+
// A failed component resolves to a null `data` rather than failing the whole
|
|
2824
|
+
// collection — but that is so the caller can REPORT what failed, not so it can
|
|
2825
|
+
// be ignored: see failedPageNames() and the export gate that uses it.
|
|
2755
2826
|
class ReportExportCollectorService {
|
|
2756
2827
|
_reportData = inject(ReportData);
|
|
2757
2828
|
_filterState = inject(ReportFilterStateService);
|
|
2758
2829
|
collect(report) {
|
|
2759
|
-
const requests = [];
|
|
2760
2830
|
const slots = [];
|
|
2761
2831
|
report.pages.forEach((page, pageIndex) => {
|
|
2762
2832
|
for (const component of page.components) {
|
|
2763
|
-
slots.push({ pageIndex });
|
|
2764
|
-
requests.push(this._componentData(report, component));
|
|
2833
|
+
slots.push({ pageIndex, component });
|
|
2765
2834
|
}
|
|
2766
2835
|
});
|
|
2767
|
-
|
|
2836
|
+
// Released a few at a time rather than all at once. mergeMap completes out
|
|
2837
|
+
// of order, so each result carries the slot it came from and the rows are
|
|
2838
|
+
// filed by index — the deck's page order can't depend on which query
|
|
2839
|
+
// happened to finish first.
|
|
2840
|
+
const all$ = slots.length
|
|
2841
|
+
? from(slots.map((slot, index) => ({ slot, index })))
|
|
2842
|
+
.pipe(mergeMap(({ slot, index }) => this._componentData(report, slot.component)
|
|
2843
|
+
.pipe(map$1((item) => ({ item, index }))), EXPORT_CONCURRENCY), toArray())
|
|
2844
|
+
: of([]);
|
|
2768
2845
|
// The summary rides alongside the component data rather than being built at
|
|
2769
2846
|
// the end, because naming a select filter's values is itself a request.
|
|
2770
2847
|
return forkJoin({ collected: all$, filterSummary: this._filterSummary(report) })
|
|
2771
2848
|
.pipe(map$1(({ collected, filterSummary }) => {
|
|
2772
2849
|
const pages = report.pages.map((page) => ({ page, components: [] }));
|
|
2773
|
-
collected
|
|
2850
|
+
[...collected]
|
|
2851
|
+
.sort((a, b) => a.index - b.index)
|
|
2852
|
+
.forEach(({ item, index }) => {
|
|
2774
2853
|
pages[slots[index].pageIndex].components.push(item);
|
|
2775
2854
|
});
|
|
2776
2855
|
return {
|
|
@@ -2789,7 +2868,10 @@ class ReportExportCollectorService {
|
|
|
2789
2868
|
const state = component.type === 'list'
|
|
2790
2869
|
? { page: 1, pageSize: LIST_EXPORT_ROWS }
|
|
2791
2870
|
: (isTimeSeries && frequency ? { frequency } : {});
|
|
2792
|
-
|
|
2871
|
+
// handlers: false keeps the app's global error dialog out of this. A failed
|
|
2872
|
+
// tile is reported once, by the export gate, naming the pages it cost —
|
|
2873
|
+
// never as ten stacked modals carrying the database's own wording.
|
|
2874
|
+
return this._reportData.componentData(report.id, component.id, filters, state, true, { handlers: false })
|
|
2793
2875
|
.pipe(map$1((data) => ({ component, data })), catchError(() => of({ component, data: null })));
|
|
2794
2876
|
}
|
|
2795
2877
|
// "Report Period: Jun 12, 2025 – Jun 12, 2026", "Organization: Alnylam - AHP,
|
|
@@ -2859,7 +2941,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2859
2941
|
// complete synchronously after setOption.
|
|
2860
2942
|
async function renderChartImage(component, data, width, height, pixelRatio = 3) {
|
|
2861
2943
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
2862
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
2944
|
+
const echarts = (await import('./firestitch-report-echarts-DeE74U3a.mjs')).default;
|
|
2863
2945
|
const host = document.createElement('div');
|
|
2864
2946
|
host.style.width = `${width}px`;
|
|
2865
2947
|
host.style.height = `${height}px`;
|
|
@@ -2953,7 +3035,7 @@ class ReportPdfService {
|
|
|
2953
3035
|
doc.setFont('helvetica', 'italic');
|
|
2954
3036
|
doc.setFontSize(9);
|
|
2955
3037
|
doc.setTextColor('#7b8794');
|
|
2956
|
-
doc.text(item
|
|
3038
|
+
doc.text(emptyLabel(item), x + w / 2, bodyY + bodyH / 2, { align: 'center' });
|
|
2957
3039
|
return;
|
|
2958
3040
|
}
|
|
2959
3041
|
switch (component.type) {
|
|
@@ -3113,7 +3195,7 @@ class ReportPptxService {
|
|
|
3113
3195
|
const bodyY = y + TITLE_H;
|
|
3114
3196
|
const bodyH = Math.max(0.3, h - TITLE_H);
|
|
3115
3197
|
if (!item.data || !item.data.rows?.length) {
|
|
3116
|
-
slide.addText(item
|
|
3198
|
+
slide.addText(emptyLabel(item), {
|
|
3117
3199
|
x, y: bodyY, w, h: bodyH,
|
|
3118
3200
|
fontSize: 10, italic: true, color: '7B8794', align: 'center', valign: 'middle',
|
|
3119
3201
|
});
|
|
@@ -3262,6 +3344,75 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3262
3344
|
type: Injectable
|
|
3263
3345
|
}] });
|
|
3264
3346
|
|
|
3347
|
+
// What the user chose when told the export is incomplete. Retry re-runs the
|
|
3348
|
+
// whole collection — the queries that failed were killed on a clock, not on
|
|
3349
|
+
// anything about the data, so a second attempt against a warmed database
|
|
3350
|
+
// genuinely often succeeds.
|
|
3351
|
+
const RETRY = 'retry';
|
|
3352
|
+
const DOWNLOAD = 'download';
|
|
3353
|
+
// Runs a report export end to end: collect every tile, refuse to write a file
|
|
3354
|
+
// that is missing any of them without saying so, then render.
|
|
3355
|
+
//
|
|
3356
|
+
// The gate is the reason this sits between the collector and the renderers
|
|
3357
|
+
// rather than in either of them. A deck with five blank pages is worse than no
|
|
3358
|
+
// deck: it looks finished, so it gets emailed to a client, and the pages that
|
|
3359
|
+
// are missing are exactly the ones nobody thinks to check. The renderers can
|
|
3360
|
+
// only describe a hole they are handed; the collector only knows one tile at a
|
|
3361
|
+
// time. Whether the FILE should exist is a decision about the whole report, and
|
|
3362
|
+
// this is the only place that sees it.
|
|
3363
|
+
class ReportExportService {
|
|
3364
|
+
_collector = inject(ReportExportCollectorService);
|
|
3365
|
+
_pdfService = inject(ReportPdfService);
|
|
3366
|
+
_pptxService = inject(ReportPptxService);
|
|
3367
|
+
_prompt = inject(FsPrompt);
|
|
3368
|
+
pdf(report) {
|
|
3369
|
+
return this._run(report, (collected) => this._pdfService.export(collected));
|
|
3370
|
+
}
|
|
3371
|
+
powerpoint(report) {
|
|
3372
|
+
return this._run(report, (collected) => this._pptxService.export(collected));
|
|
3373
|
+
}
|
|
3374
|
+
// Retry re-collects the WHOLE report, not just the tiles that failed. Every
|
|
3375
|
+
// number in one file has to have been true at one moment: refetching four
|
|
3376
|
+
// tiles and keeping nine from three minutes ago produces a deck whose pages
|
|
3377
|
+
// disagree with each other, and nothing on it would say so.
|
|
3378
|
+
_run(report, render) {
|
|
3379
|
+
return this._collector.collect(report)
|
|
3380
|
+
.pipe(switchMap((collected) => this._gate(collected)
|
|
3381
|
+
.pipe(switchMap((choice) => choice === RETRY
|
|
3382
|
+
? this._run(report, render)
|
|
3383
|
+
: from(render(collected))))));
|
|
3384
|
+
}
|
|
3385
|
+
// Resolves to DOWNLOAD when the export is whole (or the user accepted an
|
|
3386
|
+
// incomplete one) and to RETRY when they want another attempt. Cancelling
|
|
3387
|
+
// ends the export quietly: EMPTY completes without emitting, so nothing
|
|
3388
|
+
// renders and no error surfaces — they know what they just declined.
|
|
3389
|
+
_gate(collected) {
|
|
3390
|
+
const failed = failedPageNames(collected);
|
|
3391
|
+
if (!failed.length) {
|
|
3392
|
+
return of(DOWNLOAD);
|
|
3393
|
+
}
|
|
3394
|
+
const pages = failed.length === 1
|
|
3395
|
+
? `<strong>${failed[0]}</strong> could not be generated`
|
|
3396
|
+
: `${failed.length} pages could not be generated: <strong>${failed.join('</strong>, <strong>')}</strong>`;
|
|
3397
|
+
return this._prompt.confirm({
|
|
3398
|
+
title: 'Export is incomplete',
|
|
3399
|
+
template: `${pages}. This usually means those tiles took too long to load. `
|
|
3400
|
+
+ 'Retry, or download the file with those pages marked as missing.',
|
|
3401
|
+
buttons: [
|
|
3402
|
+
{ label: 'Retry', value: RETRY, color: 'primary' },
|
|
3403
|
+
{ label: 'Download Anyway', value: DOWNLOAD, color: '' },
|
|
3404
|
+
{ label: 'Cancel', cancel: true, color: '' },
|
|
3405
|
+
],
|
|
3406
|
+
})
|
|
3407
|
+
.pipe(catchError(() => EMPTY));
|
|
3408
|
+
}
|
|
3409
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportExportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
3410
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportExportService });
|
|
3411
|
+
}
|
|
3412
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportExportService, decorators: [{
|
|
3413
|
+
type: Injectable
|
|
3414
|
+
}] });
|
|
3415
|
+
|
|
3265
3416
|
// Holds the report list in memory so the picker doesn't hit the API on every
|
|
3266
3417
|
// focus. Loaded once when the page opens; create/rename/delete keep the cache
|
|
3267
3418
|
// in sync.
|
|
@@ -3339,9 +3490,7 @@ class ReportComponent {
|
|
|
3339
3490
|
_reportData = inject(ReportData);
|
|
3340
3491
|
_reportService = inject(ReportService);
|
|
3341
3492
|
_filterState = inject(ReportFilterStateService);
|
|
3342
|
-
|
|
3343
|
-
_pptxService = inject(ReportPptxService);
|
|
3344
|
-
_pdfService = inject(ReportPdfService);
|
|
3493
|
+
_exportService = inject(ReportExportService);
|
|
3345
3494
|
_process = inject(FsProcess);
|
|
3346
3495
|
_prompt = inject(FsPrompt);
|
|
3347
3496
|
_dialog = inject(MatDialog);
|
|
@@ -3420,18 +3569,34 @@ class ReportComponent {
|
|
|
3420
3569
|
if (!this.report) {
|
|
3421
3570
|
return;
|
|
3422
3571
|
}
|
|
3423
|
-
this._dialog.open(ComponentSettingsComponent, {
|
|
3572
|
+
const dialogRef = this._dialog.open(ComponentSettingsComponent, {
|
|
3424
3573
|
data: { report: this.report, component },
|
|
3425
3574
|
width: '640px',
|
|
3426
|
-
})
|
|
3427
|
-
|
|
3575
|
+
});
|
|
3576
|
+
// Filter add/remove/level changes persist immediately inside the dialog (no
|
|
3577
|
+
// "save") and can leave it by an exit that carries no result, so they are
|
|
3578
|
+
// announced as they land. The reload waits for the close so the canvas isn't
|
|
3579
|
+
// re-rendering underneath an open dialog.
|
|
3580
|
+
let filtersChanged = false;
|
|
3581
|
+
dialogRef.componentInstance.filtersChanged
|
|
3582
|
+
.pipe(takeUntil(dialogRef.afterClosed()))
|
|
3583
|
+
.subscribe(() => filtersChanged = true);
|
|
3584
|
+
dialogRef.afterClosed()
|
|
3428
3585
|
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
3429
|
-
.subscribe(() => {
|
|
3430
|
-
//
|
|
3431
|
-
//
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3586
|
+
.subscribe((result) => {
|
|
3587
|
+
// The report's filter groups changed, so the report bar and the
|
|
3588
|
+
// on-component strips have to be rebuilt from a fresh report.
|
|
3589
|
+
if (filtersChanged) {
|
|
3590
|
+
if (this.selected) {
|
|
3591
|
+
this._loadReport(this.selected.id);
|
|
3592
|
+
}
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
// Nothing else in the dialog touches the query — title and padding are
|
|
3596
|
+
// presentation. Repaint the one component it was opened for and leave
|
|
3597
|
+
// every other component's data, and its query, alone.
|
|
3598
|
+
if (result?.settings) {
|
|
3599
|
+
this._canvas?.applyComponentSettings(component.id, result.settings);
|
|
3435
3600
|
}
|
|
3436
3601
|
});
|
|
3437
3602
|
}
|
|
@@ -3450,9 +3615,7 @@ class ReportComponent {
|
|
|
3450
3615
|
if (!this.report) {
|
|
3451
3616
|
return;
|
|
3452
3617
|
}
|
|
3453
|
-
|
|
3454
|
-
this._process.run('Exporting PowerPoint', this._exportCollector.collect(report)
|
|
3455
|
-
.pipe(switchMap((collected) => from(this._pptxService.export(collected)))));
|
|
3618
|
+
this._process.run('Exporting PowerPoint', this._exportService.powerpoint(this.report));
|
|
3456
3619
|
}
|
|
3457
3620
|
// Export to PDF, entirely client-side: same collection pass, charts rendered
|
|
3458
3621
|
// through an offscreen ECharts instance at 3× pixel ratio (pixel-identical
|
|
@@ -3461,9 +3624,7 @@ class ReportComponent {
|
|
|
3461
3624
|
if (!this.report) {
|
|
3462
3625
|
return;
|
|
3463
3626
|
}
|
|
3464
|
-
|
|
3465
|
-
this._process.run('Exporting PDF', this._exportCollector.collect(report)
|
|
3466
|
-
.pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
|
|
3627
|
+
this._process.run('Exporting PDF', this._exportService.pdf(this.report));
|
|
3467
3628
|
}
|
|
3468
3629
|
// The raw data behind the whole report in one action: the server exports every
|
|
3469
3630
|
// component's full filtered dataset and zips one CSV per component, foldered
|
|
@@ -3624,9 +3785,10 @@ class ReportComponent {
|
|
|
3624
3785
|
ReportExportCollectorService,
|
|
3625
3786
|
ReportPptxService,
|
|
3626
3787
|
ReportPdfService,
|
|
3788
|
+
ReportExportService,
|
|
3627
3789
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
3628
3790
|
// this route's chunk.
|
|
3629
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
3791
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DeE74U3a.mjs').then((module) => module.default) }),
|
|
3630
3792
|
], 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 });
|
|
3631
3793
|
}
|
|
3632
3794
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
|
|
@@ -3638,9 +3800,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3638
3800
|
ReportExportCollectorService,
|
|
3639
3801
|
ReportPptxService,
|
|
3640
3802
|
ReportPdfService,
|
|
3803
|
+
ReportExportService,
|
|
3641
3804
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
3642
3805
|
// this route's chunk.
|
|
3643
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
3806
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DeE74U3a.mjs').then((module) => module.default) }),
|
|
3644
3807
|
], imports: [
|
|
3645
3808
|
FormsModule,
|
|
3646
3809
|
FsAutocompleteChipsModule,
|
|
@@ -4715,9 +4878,7 @@ class FsAiReportComponent {
|
|
|
4715
4878
|
_canvas;
|
|
4716
4879
|
_reportData = inject(ReportData);
|
|
4717
4880
|
_filterState = inject(ReportFilterStateService);
|
|
4718
|
-
|
|
4719
|
-
_pptxService = inject(ReportPptxService);
|
|
4720
|
-
_pdfService = inject(ReportPdfService);
|
|
4881
|
+
_exportService = inject(ReportExportService);
|
|
4721
4882
|
_process = inject(FsProcess);
|
|
4722
4883
|
_dialog = inject(MatDialog);
|
|
4723
4884
|
_destroyRef = inject(DestroyRef);
|
|
@@ -4777,17 +4938,33 @@ class FsAiReportComponent {
|
|
|
4777
4938
|
return;
|
|
4778
4939
|
}
|
|
4779
4940
|
const reportId = this.report.id;
|
|
4780
|
-
this._dialog.open(ComponentSettingsComponent, {
|
|
4941
|
+
const dialogRef = this._dialog.open(ComponentSettingsComponent, {
|
|
4781
4942
|
data: { report: this.report, component },
|
|
4782
4943
|
width: '640px',
|
|
4783
|
-
})
|
|
4784
|
-
|
|
4944
|
+
});
|
|
4945
|
+
// Filter add/remove/level changes persist immediately inside the dialog (no
|
|
4946
|
+
// "save") and can leave it by an exit that carries no result, so they are
|
|
4947
|
+
// announced as they land. The reload waits for the close so the canvas isn't
|
|
4948
|
+
// re-rendering underneath an open dialog.
|
|
4949
|
+
let filtersChanged = false;
|
|
4950
|
+
dialogRef.componentInstance.filtersChanged
|
|
4951
|
+
.pipe(takeUntil(dialogRef.afterClosed()))
|
|
4952
|
+
.subscribe(() => filtersChanged = true);
|
|
4953
|
+
dialogRef.afterClosed()
|
|
4785
4954
|
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
4786
|
-
.subscribe(() => {
|
|
4787
|
-
//
|
|
4788
|
-
//
|
|
4789
|
-
|
|
4790
|
-
|
|
4955
|
+
.subscribe((result) => {
|
|
4956
|
+
// The report's filter groups changed, so the report bar and the
|
|
4957
|
+
// on-component strips have to be rebuilt from a fresh report.
|
|
4958
|
+
if (filtersChanged) {
|
|
4959
|
+
this._loadReport(reportId);
|
|
4960
|
+
return;
|
|
4961
|
+
}
|
|
4962
|
+
// Nothing else in the dialog touches the query — title and padding are
|
|
4963
|
+
// presentation. Repaint the one component it was opened for and leave
|
|
4964
|
+
// every other component's data, and its query, alone.
|
|
4965
|
+
if (result?.settings) {
|
|
4966
|
+
this._canvas?.applyComponentSettings(component.id, result.settings);
|
|
4967
|
+
}
|
|
4791
4968
|
});
|
|
4792
4969
|
}
|
|
4793
4970
|
// The canvas changed the report's structure (added a page) — re-fetch so the
|
|
@@ -4803,18 +4980,14 @@ class FsAiReportComponent {
|
|
|
4803
4980
|
if (!this.report) {
|
|
4804
4981
|
return;
|
|
4805
4982
|
}
|
|
4806
|
-
|
|
4807
|
-
this._process.run('Exporting PowerPoint', this._exportCollector.collect(report)
|
|
4808
|
-
.pipe(switchMap((collected) => from(this._pptxService.export(collected)))));
|
|
4983
|
+
this._process.run('Exporting PowerPoint', this._exportService.powerpoint(this.report));
|
|
4809
4984
|
}
|
|
4810
4985
|
// Export to PDF, entirely client-side (offscreen ECharts at 3×; page = PDF page).
|
|
4811
4986
|
exportPdf() {
|
|
4812
4987
|
if (!this.report) {
|
|
4813
4988
|
return;
|
|
4814
4989
|
}
|
|
4815
|
-
|
|
4816
|
-
this._process.run('Exporting PDF', this._exportCollector.collect(report)
|
|
4817
|
-
.pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
|
|
4990
|
+
this._process.run('Exporting PDF', this._exportService.pdf(this.report));
|
|
4818
4991
|
}
|
|
4819
4992
|
// The raw data behind the whole report in one action: the server exports every
|
|
4820
4993
|
// component's full filtered dataset and zips one CSV per component, foldered
|
|
@@ -4962,8 +5135,9 @@ class FsAiReportComponent {
|
|
|
4962
5135
|
ReportExportCollectorService,
|
|
4963
5136
|
ReportPptxService,
|
|
4964
5137
|
ReportPdfService,
|
|
5138
|
+
ReportExportService,
|
|
4965
5139
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
4966
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
5140
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DeE74U3a.mjs').then((module) => module.default) }),
|
|
4967
5141
|
], 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 });
|
|
4968
5142
|
}
|
|
4969
5143
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
|
|
@@ -4975,8 +5149,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4975
5149
|
ReportExportCollectorService,
|
|
4976
5150
|
ReportPptxService,
|
|
4977
5151
|
ReportPdfService,
|
|
5152
|
+
ReportExportService,
|
|
4978
5153
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
4979
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
5154
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DeE74U3a.mjs').then((module) => module.default) }),
|
|
4980
5155
|
], imports: [
|
|
4981
5156
|
FsFilterModule,
|
|
4982
5157
|
FsMenuModule,
|
|
@@ -5010,4 +5185,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
5010
5185
|
*/
|
|
5011
5186
|
|
|
5012
5187
|
export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, ReportNodeType as f, ReportState as g, ReportStates as h };
|
|
5013
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
5188
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-Nxl-C1w0.mjs.map
|