@firestitch/report 18.19.0 → 18.20.0
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 +5 -1
- package/app/reports/interfaces/canvas-landing-input.interface.d.ts +1 -0
- package/app/reports/views/report/fs-ai-report.component.d.ts +3 -0
- package/esm2022/app/reports/canvas-landing-page.mjs +7 -1
- package/esm2022/app/reports/components/report-canvas/report-canvas.component.mjs +36 -4
- package/esm2022/app/reports/interfaces/canvas-landing-input.interface.mjs +1 -1
- package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +32 -8
- package/fesm2022/{firestitch-report-echarts-WOx5HoDM.mjs → firestitch-report-echarts-CgZBsQWf.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-WOx5HoDM.mjs.map → firestitch-report-echarts-CgZBsQWf.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-DztXL6p8.mjs → firestitch-report-firestitch-report-BQseFyHJ.mjs} +74 -13
- package/fesm2022/firestitch-report-firestitch-report-BQseFyHJ.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/firestitch-report-firestitch-report-DztXL6p8.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firestitch-report-echarts-
|
|
1
|
+
{"version":3,"file":"firestitch-report-echarts-CgZBsQWf.mjs","sources":["../../src/app/reports/theme/echarts.ts"],"sourcesContent":["// Tree-shaken ECharts build for the reports renderer: only the chart types and\n// components the option-builder emits are registered, keeping the lazy chunk\n// lean. Loaded on demand by ngx-echarts via provideEchartsCore — see\n// REPORT_ECHARTS_PROVIDERS in reports-echarts.provider.ts.\n//\n// The canvas renderer (not SVG) is deliberate: chart.getDataURL() needs it for\n// the high-res images the PPT/PDF exports will use.\n\nimport { BarChart, LineChart, PieChart } from 'echarts/charts';\nimport {\n DatasetComponent,\n GridComponent,\n LegendComponent,\n MarkAreaComponent,\n MarkLineComponent,\n TooltipComponent,\n} from 'echarts/components';\nimport * as echarts from 'echarts/core';\nimport { LabelLayout } from 'echarts/features';\nimport { CanvasRenderer } from 'echarts/renderers';\n\nimport { REPORT_CHART_COLORS_CSS } from './palette';\nimport { REPORT_AXIS_LABEL_FONT_SIZE, REPORT_CHART_FONT_FAMILY } from './typography';\n\necharts.use([\n BarChart,\n LineChart,\n PieChart,\n GridComponent,\n TooltipComponent,\n LegendComponent,\n DatasetComponent,\n // Reference lines and statistic lines (markLine), reference bands\n // (markArea), and the overlap hiding of 'auto' data labels (LabelLayout).\n MarkLineComponent,\n MarkAreaComponent,\n LabelLayout,\n CanvasRenderer,\n]);\n\n// The house theme — the single place report chart \"standards\" live (palette,\n// typography, tooltip styling). Polished defaults now; refined in the polish\n// phase.\necharts.registerTheme('report', {\n color: REPORT_CHART_COLORS_CSS,\n textStyle: {\n fontFamily: REPORT_CHART_FONT_FAMILY,\n },\n title: {\n textStyle: { color: '#1f2933', fontSize: 14, fontWeight: 600 },\n },\n legend: {\n textStyle: { color: '#52606d', fontSize: 11 },\n },\n tooltip: {\n backgroundColor: 'rgba(31, 41, 51, 0.92)',\n borderWidth: 0,\n textStyle: { color: '#fff', fontSize: 12 },\n },\n categoryAxis: {\n axisLine: { lineStyle: { color: '#cbd2d9' } },\n axisTick: { show: false },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n splitLine: { show: false },\n },\n valueAxis: {\n axisLine: { show: false },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n splitLine: { lineStyle: { color: '#e4e7eb' } },\n },\n timeAxis: {\n axisLine: { lineStyle: { color: '#cbd2d9' } },\n axisLabel: { color: '#52606d', fontSize: REPORT_AXIS_LABEL_FONT_SIZE },\n },\n});\n\nexport default echarts;\n"],"names":[],"mappings":";;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AAkBA,OAAO,CAAC,GAAG,CAAC;IACV,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,aAAa;IACb,gBAAgB;IAChB,eAAe;IACf,gBAAgB;;;IAGhB,iBAAiB;IACjB,iBAAiB;IACjB,WAAW;IACX,cAAc;AACf,CAAA,CAAC,CAAC;AAEH;AACA;AACA;AACA,OAAO,CAAC,aAAa,CAAC,QAAQ,EAAE;AAC9B,IAAA,KAAK,EAAE,uBAAuB;AAC9B,IAAA,SAAS,EAAE;AACT,QAAA,UAAU,EAAE,wBAAwB;AACrC,KAAA;AACD,IAAA,KAAK,EAAE;AACL,QAAA,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE;AAC/D,KAAA;AACD,IAAA,MAAM,EAAE;QACN,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE;AAC9C,KAAA;AACD,IAAA,OAAO,EAAE;AACP,QAAA,eAAe,EAAE,wBAAwB;AACzC,QAAA,WAAW,EAAE,CAAC;QACd,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE;AAC3C,KAAA;AACD,IAAA,YAAY,EAAE;QACZ,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;AAC7C,QAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;QACzB,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;AACtE,QAAA,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;AAC3B,KAAA;AACD,IAAA,SAAS,EAAE;AACT,QAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;QACzB,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;QACtE,SAAS,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;AAC/C,KAAA;AACD,IAAA,QAAQ,EAAE;QACR,QAAQ,EAAE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;QAC7C,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,2BAA2B,EAAE;AACvE,KAAA;AACF,CAAA,CAAC"}
|
|
@@ -1957,6 +1957,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
1957
1957
|
|
|
1958
1958
|
// Which page the canvas opens when its report input changes.
|
|
1959
1959
|
//
|
|
1960
|
+
// - A Refresh keeps the page that was on screen, by id, wherever it moved; when
|
|
1961
|
+
// it is gone, the first page. A page that arrived with the refresh was added
|
|
1962
|
+
// elsewhere (the MCP, another tab), so it does not take the viewer there.
|
|
1960
1963
|
// - The same report with more pages than before: a page was just added in
|
|
1961
1964
|
// this tab, so the canvas lands on the last page. The asked-for page does
|
|
1962
1965
|
// not beat it: that is a stale value the host still holds.
|
|
@@ -1970,6 +1973,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
1970
1973
|
function canvasLandingPage(input) {
|
|
1971
1974
|
const pageCount = input.pageIds.length;
|
|
1972
1975
|
const sameReport = input.reportId === input.previousReportId;
|
|
1976
|
+
if (input.keepPageId !== null) {
|
|
1977
|
+
return Math.max(input.pageIds.indexOf(input.keepPageId), 0);
|
|
1978
|
+
}
|
|
1973
1979
|
if (sameReport && input.previousPageCount > 0 && pageCount > input.previousPageCount) {
|
|
1974
1980
|
return pageCount - 1;
|
|
1975
1981
|
}
|
|
@@ -12597,6 +12603,9 @@ class ReportCanvasComponent {
|
|
|
12597
12603
|
// survives pages being reordered or deleted; an id that no longer resolves
|
|
12598
12604
|
// falls back to the first page.
|
|
12599
12605
|
pageId = null;
|
|
12606
|
+
// The host is reloading the report a Refresh asked for: the button shows a
|
|
12607
|
+
// spinner and takes no second press until it lands.
|
|
12608
|
+
busy = false;
|
|
12600
12609
|
// A component asked for its settings dialog — the page-level host opens it
|
|
12601
12610
|
// (it owns MatDialog + the post-save reload).
|
|
12602
12611
|
componentSettings = new EventEmitter();
|
|
@@ -12606,6 +12615,9 @@ class ReportCanvasComponent {
|
|
|
12606
12615
|
// The page on screen changed — the host keeps it in the report's state.
|
|
12607
12616
|
// Carries the page id, never the index.
|
|
12608
12617
|
pageChanged = new EventEmitter();
|
|
12618
|
+
// Refresh was pressed — the host reloads the report (it owns the load), and
|
|
12619
|
+
// every tile refetches as the reloaded report re-inits the filter state.
|
|
12620
|
+
refresh = new EventEmitter();
|
|
12609
12621
|
zoomPan;
|
|
12610
12622
|
// The rendered components of the active page — only the canvas can reach the
|
|
12611
12623
|
// one instance a settings save applies to.
|
|
@@ -12674,6 +12686,10 @@ class ReportCanvasComponent {
|
|
|
12674
12686
|
// A text block added by double-clicking a band, whose editor opens once the
|
|
12675
12687
|
// reloaded report draws it.
|
|
12676
12688
|
_editOnArrival = null;
|
|
12689
|
+
// The page on screen when Refresh was pressed, which the reloaded report
|
|
12690
|
+
// lands on by id (canvasLandingPage). Held until the refresh ends (busy
|
|
12691
|
+
// goes off), whether it brought a report or failed.
|
|
12692
|
+
_keepPageId = null;
|
|
12677
12693
|
// The report input as a signal, for the token contexts and the master's
|
|
12678
12694
|
// drawn components. Notifies on every set: a reload hands over a new report
|
|
12679
12695
|
// even when nothing a token shows moved, and a deleted header or footer
|
|
@@ -12742,9 +12758,13 @@ class ReportCanvasComponent {
|
|
|
12742
12758
|
pageIds: this.pages().map((page) => page.id),
|
|
12743
12759
|
pageId: this.pageId,
|
|
12744
12760
|
activePageIndex: this.activePageIndex(),
|
|
12761
|
+
keepPageId: this._keepPageId,
|
|
12745
12762
|
}));
|
|
12746
12763
|
this._previousPageCount = this.pages().length;
|
|
12747
12764
|
this._previousReportId = reportId;
|
|
12765
|
+
if (changes.busy && !this.busy) {
|
|
12766
|
+
this._keepPageId = null;
|
|
12767
|
+
}
|
|
12748
12768
|
// Tell the host which page is open whenever that changes — including the
|
|
12749
12769
|
// FIRST time the report renders, not only when someone clicks a page.
|
|
12750
12770
|
// Hosts bind things to the open page (the report chat scopes its
|
|
@@ -13040,7 +13060,19 @@ class ReportCanvasComponent {
|
|
|
13040
13060
|
this.selectComponent(null);
|
|
13041
13061
|
}
|
|
13042
13062
|
}
|
|
13043
|
-
// ----- zoom toolbar
|
|
13063
|
+
// ----- refresh + zoom toolbar -----------------------------------------------
|
|
13064
|
+
// The report again, for changes made away from this screen (the MCP, another
|
|
13065
|
+
// tab) and for data that moved. The text blocks' values are read again now:
|
|
13066
|
+
// a reload only reads them when the report it brings moves their request.
|
|
13067
|
+
refreshReport() {
|
|
13068
|
+
if (this.busy) {
|
|
13069
|
+
return;
|
|
13070
|
+
}
|
|
13071
|
+
this._keepPageId = this.activePageId();
|
|
13072
|
+
this._pageValues.reload$.next();
|
|
13073
|
+
this._masterValues.reload$.next();
|
|
13074
|
+
this.refresh.emit();
|
|
13075
|
+
}
|
|
13044
13076
|
onZoomed(scale) {
|
|
13045
13077
|
this.zoom.set(scale);
|
|
13046
13078
|
}
|
|
@@ -13431,30 +13463,35 @@ class ReportCanvasComponent {
|
|
|
13431
13463
|
this._guides?.nativeElement?.replaceChildren();
|
|
13432
13464
|
}
|
|
13433
13465
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13434
|
-
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", pageId: "pageId" }, outputs: { componentSettings: "componentSettings", reportChanged: "reportChanged", pageChanged: "pageChanged" }, 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 wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\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 (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. While the\n header and footer are edited, an empty band says how to fill it\n and a double-click adds a text block there. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.headerHeight * PX\"\n (dblclick)=\"addTextInBand('header', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n @if (isBandEmpty('header')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.footerHeight * PX\"\n (dblclick)=\"addTextInBand('footer', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n @if (isBandEmpty('footer')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\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 (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\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 </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\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 (Ctrl/Cmd + scroll)\"\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 (Ctrl/Cmd + scroll)\"\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;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;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 .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 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 .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.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-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.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%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band.addable{pointer-events:auto;cursor:text;-webkit-user-select:none;user-select:none}.band .band-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));color:#9aa5b1;white-space:nowrap}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.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: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$5.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$5.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { 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: 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: "ngmodule", type: FsZoomPanModule }, { kind: "component", type: i3$1.FsZoomPanComponent, selector: "fs-zoom-pan", inputs: ["zoomMax", "zoomMin", "zoomScale", "zoomDefault", "zoomFactor", "zoomFit", "fitMode", "fitPosition", "fitPadding", "fitMaxScale", "wheel", "top", "left"], outputs: ["moved", "zoomed"] }, { kind: "component", type: ReportComponentComponent, selector: "app-report-component", inputs: ["reportId", "component", "groups", "editMode", "zoom", "snapper", "selected", "tokens", "valueKeysElsewhere"], outputs: ["selectComponent", "positionChanged", "openSettings", "removed", "valueSettings", "textSaved", "valuesRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13466
|
+
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", pageId: "pageId", busy: "busy" }, outputs: { componentSettings: "componentSettings", reportChanged: "reportChanged", pageChanged: "pageChanged", refresh: "refresh" }, 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 wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\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 (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. While the\n header and footer are edited, an empty band says how to fill it\n and a double-click adds a text block there. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.headerHeight * PX\"\n (dblclick)=\"addTextInBand('header', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n @if (isBandEmpty('header')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.footerHeight * PX\"\n (dblclick)=\"addTextInBand('footer', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n @if (isBandEmpty('footer')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\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 (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\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 </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\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 <!-- Refresh sits to the left of the zoom, in a pill of its own: both act\n on the whole page in view, but Refresh is not a zoom step. -->\n <div class=\"view-controls\">\n <div class=\"control-pill\">\n <button\n type=\"button\"\n class=\"control-button\"\n matTooltip=\"Refresh\"\n [disabled]=\"busy\"\n (click)=\"refreshReport()\">\n @if (busy) {\n <mat-spinner diameter=\"20\"></mat-spinner>\n } @else {\n <mat-icon>refresh</mat-icon>\n }\n </button>\n </div>\n\n <div class=\"control-pill\">\n <button\n type=\"button\"\n class=\"control-button\"\n matTooltip=\"Zoom out (Ctrl/Cmd + scroll)\"\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=\"control-button\"\n matTooltip=\"Zoom in (Ctrl/Cmd + scroll)\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"control-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\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;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;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 .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 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 .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.canvas-toolbar .view-controls{display:flex;align-items:center;gap:4px;margin-left:auto}.canvas-toolbar .control-pill{display:flex;align-items:center;gap:2px;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .control-pill .control-button{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .control-pill .control-button:hover:not(:disabled){background:#f0f4f8}.canvas-toolbar .control-pill .control-button:disabled{cursor:default}.canvas-toolbar .control-pill .control-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .control-pill .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .control-pill .zoom-label:hover{color:#1f2933}.page-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.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%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band.addable{pointer-events:auto;cursor:text;-webkit-user-select:none;user-select:none}.band .band-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));color:#9aa5b1;white-space:nowrap}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.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: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$5.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$5.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { 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: "ngmodule", type: FsZoomPanModule }, { kind: "component", type: i3$1.FsZoomPanComponent, selector: "fs-zoom-pan", inputs: ["zoomMax", "zoomMin", "zoomScale", "zoomDefault", "zoomFactor", "zoomFit", "fitMode", "fitPosition", "fitPadding", "fitMaxScale", "wheel", "top", "left"], outputs: ["moved", "zoomed"] }, { kind: "component", type: ReportComponentComponent, selector: "app-report-component", inputs: ["reportId", "component", "groups", "editMode", "zoom", "snapper", "selected", "tokens", "valueKeysElsewhere"], outputs: ["selectComponent", "positionChanged", "openSettings", "removed", "valueSettings", "textSaved", "valuesRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13435
13467
|
}
|
|
13436
13468
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportCanvasComponent, decorators: [{
|
|
13437
13469
|
type: Component,
|
|
13438
13470
|
args: [{ selector: 'app-report-canvas', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
|
|
13439
13471
|
DragDropModule,
|
|
13440
13472
|
MatIcon,
|
|
13473
|
+
MatProgressSpinner,
|
|
13441
13474
|
MatTooltip,
|
|
13442
13475
|
FsMenuModule,
|
|
13443
13476
|
FsZoomPanModule,
|
|
13444
13477
|
ReportComponentComponent,
|
|
13445
|
-
], template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\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 (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. While the\n header and footer are edited, an empty band says how to fill it\n and a double-click adds a text block there. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.headerHeight * PX\"\n (dblclick)=\"addTextInBand('header', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n @if (isBandEmpty('header')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.footerHeight * PX\"\n (dblclick)=\"addTextInBand('footer', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n @if (isBandEmpty('footer')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\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 (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\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 </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\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 (Ctrl/Cmd + scroll)\"\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 (Ctrl/Cmd + scroll)\"\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;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;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 .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 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 .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.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-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.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%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band.addable{pointer-events:auto;cursor:text;-webkit-user-select:none;user-select:none}.band .band-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));color:#9aa5b1;white-space:nowrap}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.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"] }]
|
|
13478
|
+
], template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\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 (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. While the\n header and footer are edited, an empty band says how to fill it\n and a double-click adds a text block there. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.headerHeight * PX\"\n (dblclick)=\"addTextInBand('header', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n @if (isBandEmpty('header')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.footerHeight * PX\"\n (dblclick)=\"addTextInBand('footer', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n @if (isBandEmpty('footer')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\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 (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\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 </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\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 <!-- Refresh sits to the left of the zoom, in a pill of its own: both act\n on the whole page in view, but Refresh is not a zoom step. -->\n <div class=\"view-controls\">\n <div class=\"control-pill\">\n <button\n type=\"button\"\n class=\"control-button\"\n matTooltip=\"Refresh\"\n [disabled]=\"busy\"\n (click)=\"refreshReport()\">\n @if (busy) {\n <mat-spinner diameter=\"20\"></mat-spinner>\n } @else {\n <mat-icon>refresh</mat-icon>\n }\n </button>\n </div>\n\n <div class=\"control-pill\">\n <button\n type=\"button\"\n class=\"control-button\"\n matTooltip=\"Zoom out (Ctrl/Cmd + scroll)\"\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=\"control-button\"\n matTooltip=\"Zoom in (Ctrl/Cmd + scroll)\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"control-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\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;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;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 .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 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 .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.canvas-toolbar .view-controls{display:flex;align-items:center;gap:4px;margin-left:auto}.canvas-toolbar .control-pill{display:flex;align-items:center;gap:2px;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .control-pill .control-button{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .control-pill .control-button:hover:not(:disabled){background:#f0f4f8}.canvas-toolbar .control-pill .control-button:disabled{cursor:default}.canvas-toolbar .control-pill .control-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .control-pill .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .control-pill .zoom-label:hover{color:#1f2933}.page-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.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%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band.addable{pointer-events:auto;cursor:text;-webkit-user-select:none;user-select:none}.band .band-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));color:#9aa5b1;white-space:nowrap}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.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"] }]
|
|
13446
13479
|
}], ctorParameters: () => [], propDecorators: { report: [{
|
|
13447
13480
|
type: Input
|
|
13448
13481
|
}], editMode: [{
|
|
13449
13482
|
type: Input
|
|
13450
13483
|
}], pageId: [{
|
|
13451
13484
|
type: Input
|
|
13485
|
+
}], busy: [{
|
|
13486
|
+
type: Input
|
|
13452
13487
|
}], componentSettings: [{
|
|
13453
13488
|
type: Output
|
|
13454
13489
|
}], reportChanged: [{
|
|
13455
13490
|
type: Output
|
|
13456
13491
|
}], pageChanged: [{
|
|
13457
13492
|
type: Output
|
|
13493
|
+
}], refresh: [{
|
|
13494
|
+
type: Output
|
|
13458
13495
|
}], zoomPan: [{
|
|
13459
13496
|
type: ViewChild,
|
|
13460
13497
|
args: [FsZoomPanComponent]
|
|
@@ -15767,7 +15804,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
15767
15804
|
// in the report's format context, as on screen.
|
|
15768
15805
|
async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
|
|
15769
15806
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
15770
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
15807
|
+
const echarts = (await import('./firestitch-report-echarts-CgZBsQWf.mjs')).default;
|
|
15771
15808
|
const host = document.createElement('div');
|
|
15772
15809
|
host.style.width = `${width}px`;
|
|
15773
15810
|
host.style.height = `${height}px`;
|
|
@@ -18823,6 +18860,8 @@ class FsAiReportComponent {
|
|
|
18823
18860
|
pageChanged = output();
|
|
18824
18861
|
report = signal(null);
|
|
18825
18862
|
editMode = signal(false);
|
|
18863
|
+
// A Refresh is reloading the report; the canvas's button spins until it lands.
|
|
18864
|
+
busy = signal(false);
|
|
18826
18865
|
// The page the canvas should open on: the ?page= the report opened with
|
|
18827
18866
|
// (read once per report). Cleared as soon as the canvas reports back, so a
|
|
18828
18867
|
// later in-app page change is never fought by a stale value.
|
|
@@ -19069,6 +19108,21 @@ class FsAiReportComponent {
|
|
|
19069
19108
|
this._loadReport(report.id);
|
|
19070
19109
|
}
|
|
19071
19110
|
}
|
|
19111
|
+
// Refresh, from the canvas: the report again, for changes made away from this
|
|
19112
|
+
// screen (the MCP, another tab). The load re-inits the filter state, which
|
|
19113
|
+
// refetches every tile in place, and keeps the viewer's filters, zoom and
|
|
19114
|
+
// chat; the canvas keeps the page on screen. A rename comes back with it, so
|
|
19115
|
+
// the host hears of one.
|
|
19116
|
+
onCanvasRefresh() {
|
|
19117
|
+
const report = this.report();
|
|
19118
|
+
if (!report || this.busy()) {
|
|
19119
|
+
return;
|
|
19120
|
+
}
|
|
19121
|
+
this.busy.set(true);
|
|
19122
|
+
this._reportLoad(report.id, true)
|
|
19123
|
+
.pipe(finalize(() => this.busy.set(false)), takeUntilDestroyed(this._destroyRef))
|
|
19124
|
+
.subscribe();
|
|
19125
|
+
}
|
|
19072
19126
|
// Export the report to PowerPoint, entirely client-side (native editable
|
|
19073
19127
|
// charts; page = slide). The dock shows progress until the file saves.
|
|
19074
19128
|
exportPowerpoint() {
|
|
@@ -19217,15 +19271,22 @@ class FsAiReportComponent {
|
|
|
19217
19271
|
// load opens it on its authored defaults, and a later one (a chat edit, a
|
|
19218
19272
|
// settings save) keeps the state in memory.
|
|
19219
19273
|
_loadReport(reportId, emitRenamed = false) {
|
|
19220
|
-
this.
|
|
19221
|
-
.pipe(
|
|
19222
|
-
.subscribe(
|
|
19274
|
+
this._reportLoad(reportId, emitRenamed)
|
|
19275
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
19276
|
+
.subscribe();
|
|
19277
|
+
}
|
|
19278
|
+
// One load of the report, applied as it lands (_loadReport). Refresh
|
|
19279
|
+
// subscribes to it directly to know when it ends.
|
|
19280
|
+
_reportLoad(reportId, emitRenamed) {
|
|
19281
|
+
return this._reportData.get(reportId)
|
|
19282
|
+
.pipe(tap((report) => {
|
|
19283
|
+
this._filterState.init(report);
|
|
19223
19284
|
this.report.set(report);
|
|
19224
19285
|
this._buildReportConfig();
|
|
19225
19286
|
if (emitRenamed) {
|
|
19226
19287
|
this.renamed.emit(report);
|
|
19227
19288
|
}
|
|
19228
|
-
});
|
|
19289
|
+
}));
|
|
19229
19290
|
}
|
|
19230
19291
|
// Report-level filter groups in their saved order — the ones that render in
|
|
19231
19292
|
// the toolbar (component-level groups render on their components), with
|
|
@@ -19432,11 +19493,11 @@ class FsAiReportComponent {
|
|
|
19432
19493
|
ReportAiChatData,
|
|
19433
19494
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
19434
19495
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
19435
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19496
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CgZBsQWf.mjs').then((module) => module.default) }),
|
|
19436
19497
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
19437
19498
|
// presets; a preset only sets the days.
|
|
19438
19499
|
provideReportDatePicker(),
|
|
19439
|
-
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }, { propertyName: "_reportBar", first: true, predicate: FilterComponent, 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, with the\n report's saved filters in fs-filter's own Saved filters menu.\n 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\n block is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportData(false, 'xlsx')\">\n Export Excel\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true, 'xlsx')\">\n Export Excel (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportPackage()\">\n Export Report Package\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 @if (chatEnabled()) {\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"chatBasePath()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n }\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$3.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", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
19500
|
+
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }, { propertyName: "_reportBar", first: true, predicate: FilterComponent, 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, with the\n report's saved filters in fs-filter's own Saved filters menu.\n 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\n block is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportData(false, 'xlsx')\">\n Export Excel\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true, 'xlsx')\">\n Export Excel (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportPackage()\">\n Export Report Package\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 @if (chatEnabled()) {\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"chatBasePath()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n }\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n [busy]=\"busy()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\"\n (refresh)=\"onCanvasRefresh()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$3.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", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId", "busy"], outputs: ["componentSettings", "reportChanged", "pageChanged", "refresh"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
19440
19501
|
}
|
|
19441
19502
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
|
|
19442
19503
|
type: Component,
|
|
@@ -19456,7 +19517,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19456
19517
|
ReportAiChatData,
|
|
19457
19518
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
19458
19519
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
19459
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19520
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CgZBsQWf.mjs').then((module) => module.default) }),
|
|
19460
19521
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
19461
19522
|
// presets; a preset only sets the days.
|
|
19462
19523
|
provideReportDatePicker(),
|
|
@@ -19468,7 +19529,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19468
19529
|
MatIconModule,
|
|
19469
19530
|
MatTooltipModule,
|
|
19470
19531
|
ReportCanvasComponent,
|
|
19471
|
-
], 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, with the\n report's saved filters in fs-filter's own Saved filters menu.\n 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\n block is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportData(false, 'xlsx')\">\n Export Excel\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true, 'xlsx')\">\n Export Excel (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportPackage()\">\n Export Report Package\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 @if (chatEnabled()) {\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"chatBasePath()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n }\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
|
|
19532
|
+
], 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, with the\n report's saved filters in fs-filter's own Saved filters menu.\n 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\n block is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportData(false, 'xlsx')\">\n Export Excel\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true, 'xlsx')\">\n Export Excel (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportPackage()\">\n Export Report Package\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 @if (chatEnabled()) {\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"chatBasePath()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n }\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n [busy]=\"busy()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\"\n (refresh)=\"onCanvasRefresh()\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
|
|
19472
19533
|
}], ctorParameters: () => [], propDecorators: { _split: [{
|
|
19473
19534
|
type: ViewChild,
|
|
19474
19535
|
args: ['split', { static: true }]
|
|
@@ -19549,4 +19610,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19549
19610
|
*/
|
|
19550
19611
|
|
|
19551
19612
|
export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, REPORT_CHART_FONT_FAMILY as a, REPORT_AXIS_LABEL_FONT_SIZE as b, FsAiReportComponent as c, ReportData as d, ReportService as e, ReportFilterStateService as f, ReportNodeType as g, ReportState as h, ReportValueFormat as i, ReportValueFormula as j, ReportListColumnFormat as k, ReportNumberUnits as l, ReportChartDataLabels as m, ReportChartColor as n, ReportChartLineStyle as o, ReportChartMarkStat as p, ReportChartDateLabel as q, ReportChartSpanThrough as r, ReportChangePolarity as s };
|
|
19552
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
19613
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-BQseFyHJ.mjs.map
|