@firestitch/report 18.21.2 → 18.21.4
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/export/report-export-collector.service.d.ts +1 -8
- package/app/reports/export/report-export.service.d.ts +0 -1
- package/app/reports/export/report-pdf.service.d.ts +0 -1
- package/app/reports/export/report-pptx.service.d.ts +0 -2
- package/app/reports/export/report-text-layout.service.d.ts +0 -4
- package/esm2022/app/reports/components/component-grid/report-grid-formatter.mjs +23 -6
- package/esm2022/app/reports/components/component-text/component-text.component.mjs +3 -3
- package/esm2022/app/reports/export/report-export-collector.service.mjs +9 -15
- package/esm2022/app/reports/export/report-export.service.mjs +7 -34
- package/esm2022/app/reports/export/report-pdf.service.mjs +20 -39
- package/esm2022/app/reports/export/report-pptx.service.mjs +60 -101
- package/esm2022/app/reports/export/report-text-layout.service.mjs +5 -20
- package/fesm2022/{firestitch-report-echarts-BLr6SmeK.mjs → firestitch-report-echarts-CBBTk1Ql.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-BLr6SmeK.mjs.map → firestitch-report-echarts-CBBTk1Ql.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-D2dPznfE.mjs → firestitch-report-firestitch-report-Cl_JOBli.mjs} +121 -212
- package/fesm2022/firestitch-report-firestitch-report-Cl_JOBli.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/firestitch-report-firestitch-report-D2dPznfE.mjs.map +0 -1
|
@@ -9962,6 +9962,18 @@ const NUMBER_UNITS = {
|
|
|
9962
9962
|
};
|
|
9963
9963
|
// What an abbreviated number's suffix multiplies it by, when a viewer types one.
|
|
9964
9964
|
const UNIT_SUFFIXES = { k: 1e3, m: 1e6 };
|
|
9965
|
+
// A column written in thousands or millions is typed in them: the grid's
|
|
9966
|
+
// filter shows its K or M beside the inputs, so 12 there means 12,000. A K or
|
|
9967
|
+
// M typed after the digits wins, so 12K is still 12,000.
|
|
9968
|
+
const TYPED_UNITS = {
|
|
9969
|
+
[GridUnits.Thousands]: 1e3,
|
|
9970
|
+
[GridUnits.Millions]: 1e6,
|
|
9971
|
+
};
|
|
9972
|
+
// The significant digits a scaled typed number keeps, as fs-grid's own reader
|
|
9973
|
+
// keeps them: scaling leaves binary noise behind (12.3% reads
|
|
9974
|
+
// 0.12300000000000001, 1.005K 1004.9999999999999), which would move a range's
|
|
9975
|
+
// end past the very value it names.
|
|
9976
|
+
const TYPED_DIGITS = 15;
|
|
9965
9977
|
const YES = 'Yes';
|
|
9966
9978
|
const NO = 'No';
|
|
9967
9979
|
// A grid tile's formatter: every value written as the report writes it on
|
|
@@ -10039,20 +10051,25 @@ function optionLabel(value, field) {
|
|
|
10039
10051
|
return field.options?.find((option) => option.key === value)?.label ?? null;
|
|
10040
10052
|
}
|
|
10041
10053
|
// A number a viewer typed, read in the report's locale: its grouping dropped,
|
|
10042
|
-
// its decimal mark read, a K or M suffix multiplied out
|
|
10043
|
-
//
|
|
10054
|
+
// its decimal mark read, a K or M suffix multiplied out (else the column's own
|
|
10055
|
+
// thousands or millions), and a percent of a ratio-percent report read back to
|
|
10056
|
+
// its ratio. The placement's units and format win over the field's, as they do
|
|
10057
|
+
// where the number is written (numberSpec). null when it is no number.
|
|
10044
10058
|
function readGridNumber(context, text, field, placement) {
|
|
10045
10059
|
const decimal = decimalMark(context.format.locale);
|
|
10046
10060
|
const trimmed = text.trim().toLowerCase();
|
|
10047
|
-
const multiplier = UNIT_SUFFIXES[trimmed.slice(-1)]
|
|
10061
|
+
const multiplier = UNIT_SUFFIXES[trimmed.slice(-1)]
|
|
10062
|
+
?? TYPED_UNITS[placement?.units ?? field.units ?? GridUnits.None]
|
|
10063
|
+
?? 1;
|
|
10048
10064
|
const digits = trimmed
|
|
10049
10065
|
.split(decimal).map((part) => part.replace(/[^0-9-]/g, '')).join('.');
|
|
10050
10066
|
const number = digits && digits !== '-' && digits !== '.' ? Number(digits) * multiplier : NaN;
|
|
10051
10067
|
if (!Number.isFinite(number)) {
|
|
10052
10068
|
return null;
|
|
10053
10069
|
}
|
|
10054
|
-
const
|
|
10055
|
-
|
|
10070
|
+
const isRatio = (placement?.format ?? field.format) === GridFormat.Percent && context.format.ratioPercent;
|
|
10071
|
+
const value = isRatio ? number / 100 : number;
|
|
10072
|
+
return multiplier === 1 && !isRatio ? value : Number(value.toPrecision(TYPED_DIGITS));
|
|
10056
10073
|
}
|
|
10057
10074
|
function decimalMark(locale) {
|
|
10058
10075
|
return numberFormat(locale, {}).formatToParts(1.5).find((part) => part.type === 'decimal')?.value ?? '.';
|
|
@@ -11754,7 +11771,7 @@ class ComponentTextComponent {
|
|
|
11754
11771
|
}
|
|
11755
11772
|
}
|
|
11756
11773
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11757
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentTextComponent, isStandalone: true, selector: "app-report-component-text", inputs: { html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: true, transformFunction: null }, editing: { classPropertyName: "editing", publicName: "editing", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, shownElsewhere: { classPropertyName: "shownElsewhere", publicName: "shownElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { saved: "saved", editEnded: "editEnded", valueSettings: "valueSettings" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "style.--text-size": "typeScale.body", "style.--text-h1-size": "typeScale.h1", "style.--text-h2-size": "typeScale.h2", "style.--text-h3-size": "typeScale.h3", "style.--text-color": "typeScale.color", "style.--text-table-border": "tableLook.border", "style.--text-table-padding": "tableLook.padding", "style.--text-table-margin": "tableLook.margin" } }, viewQueries: [{ propertyName: "_editor", first: true, predicate: FsHtmlEditorComponent, descendants: true, isSignal: true }, { propertyName: "_valuePopoverAnchor", first: true, predicate: ["valuePopoverAnchor"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else if (editMode() && isBlank()) {\n <!-- An empty block while the canvas is laid out: a hint in the block's own\n type, never its html, so nothing is typed onto it or exported. -->\n <div class=\"text-content text-hint\">Double-click to edit</div>\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:
|
|
11774
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentTextComponent, isStandalone: true, selector: "app-report-component-text", inputs: { html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: true, transformFunction: null }, editing: { classPropertyName: "editing", publicName: "editing", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, shownElsewhere: { classPropertyName: "shownElsewhere", publicName: "shownElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { saved: "saved", editEnded: "editEnded", valueSettings: "valueSettings" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "style.--text-size": "typeScale.body", "style.--text-h1-size": "typeScale.h1", "style.--text-h2-size": "typeScale.h2", "style.--text-h3-size": "typeScale.h3", "style.--text-color": "typeScale.color", "style.--text-table-border": "tableLook.border", "style.--text-table-padding": "tableLook.padding", "style.--text-table-margin": "tableLook.margin" } }, viewQueries: [{ propertyName: "_editor", first: true, predicate: FsHtmlEditorComponent, descendants: true, isSignal: true }, { propertyName: "_valuePopoverAnchor", first: true, predicate: ["valuePopoverAnchor"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else if (editMode() && isBlank()) {\n <!-- An empty block while the canvas is laid out: a hint in the block's own\n type, never its html, so nothing is typed onto it or exported. -->\n <div class=\"text-content text-hint\">Double-click to edit</div>\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:visible;font-family:Arial,Helvetica,sans-serif;font-size:var(--text-size);line-height:1.25;color:var(--text-color);word-wrap:break-word;letter-spacing:normal;word-spacing:normal;font-weight:400;font-style:normal;text-transform:none;white-space:normal}.text-content ::ng-deep p,.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3,.text-content ::ng-deep ul,.text-content ::ng-deep ol{margin:0}.text-content ::ng-deep h1{font-size:var(--text-h1-size)}.text-content ::ng-deep h2{font-size:var(--text-h2-size)}.text-content ::ng-deep h3{font-size:var(--text-h3-size)}.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3{font-weight:700;line-height:1.2}.text-content ::ng-deep ul,.text-content ::ng-deep ol{padding-left:1.4em}.text-content ::ng-deep p:empty:before{content:\"\\200b\"}.text-content ::ng-deep a{color:inherit}.text-content ::ng-deep table{border-collapse:collapse;margin:var(--text-table-margin) 0}.text-content ::ng-deep th,.text-content ::ng-deep td{border:var(--text-table-border);padding:var(--text-table-padding);vertical-align:middle;text-align:left}.text-content ::ng-deep th{text-align:center;font-weight:700}.text-content.editing{cursor:text}.text-content.editing ::ng-deep [data-embed-key],.text-content.editing ::ng-deep [data-embed-field]{padding:0 2px;border-radius:3px;background-color:#4e79a729;-webkit-user-select:all;user-select:all}.text-content.editing ::ng-deep [data-embed-key]{cursor:pointer}.text-content.editing ::ng-deep [data-embed-key]:hover{background-color:#4e79a747}.text-content.editing ::ng-deep [data-embed-missing]{background-color:#e1575924;outline:1px dashed rgba(225,87,89,.7)}.text-content.editing ::ng-deep th,.text-content.editing ::ng-deep tfoot td{background-color:transparent}.text-content.editing ::ng-deep .fr-table-selector{display:none}.text-content.editing ::ng-deep .fr-table-selection-hover{outline:none}.text-hint{color:#9aa5b1;-webkit-user-select:none;user-select:none}.value-popover-anchor{position:absolute;pointer-events:none}::ng-deep .value-popover-wrapper .popover-content{padding:15px!important}.value-popover{gap:15px}.value-popover-name{font-weight:600}.value-popover-settings{--mdc-icon-button-state-layer-size: 24px;width:24px;height:24px;padding:0}.value-notice{position:absolute;top:100%;left:0;z-index:2;margin-top:4px;padding:2px 4px 2px 10px;border:1px solid #d9dee3;border-radius:4px;background:#fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: FsHtmlEditorComponent, selector: "fs-html-editor", inputs: ["config", "disabled"] }, { kind: "directive", type: FsPopoverDirective, selector: "[fsPopover]", inputs: ["text", "template", "data", "leaveDelay", "showDelay", "maxWidth", "maxHeight", "wrapperClass", "autoShow", "autoClose", "loadingDiameter", "loading", "indication", "position", "theme", "size", "trigger", "enabled", "panel"], outputs: ["closed"], exportAs: ["fsPopover"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11758
11775
|
}
|
|
11759
11776
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentTextComponent, decorators: [{
|
|
11760
11777
|
type: Component,
|
|
@@ -11779,7 +11796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
11779
11796
|
'[style.--text-table-border]': 'tableLook.border',
|
|
11780
11797
|
'[style.--text-table-padding]': 'tableLook.padding',
|
|
11781
11798
|
'[style.--text-table-margin]': 'tableLook.margin',
|
|
11782
|
-
}, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else if (editMode() && isBlank()) {\n <!-- An empty block while the canvas is laid out: a hint in the block's own\n type, never its html, so nothing is typed onto it or exported. -->\n <div class=\"text-content text-hint\">Double-click to edit</div>\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:
|
|
11799
|
+
}, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else if (editMode() && isBlank()) {\n <!-- An empty block while the canvas is laid out: a hint in the block's own\n type, never its html, so nothing is typed onto it or exported. -->\n <div class=\"text-content text-hint\">Double-click to edit</div>\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:visible;font-family:Arial,Helvetica,sans-serif;font-size:var(--text-size);line-height:1.25;color:var(--text-color);word-wrap:break-word;letter-spacing:normal;word-spacing:normal;font-weight:400;font-style:normal;text-transform:none;white-space:normal}.text-content ::ng-deep p,.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3,.text-content ::ng-deep ul,.text-content ::ng-deep ol{margin:0}.text-content ::ng-deep h1{font-size:var(--text-h1-size)}.text-content ::ng-deep h2{font-size:var(--text-h2-size)}.text-content ::ng-deep h3{font-size:var(--text-h3-size)}.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3{font-weight:700;line-height:1.2}.text-content ::ng-deep ul,.text-content ::ng-deep ol{padding-left:1.4em}.text-content ::ng-deep p:empty:before{content:\"\\200b\"}.text-content ::ng-deep a{color:inherit}.text-content ::ng-deep table{border-collapse:collapse;margin:var(--text-table-margin) 0}.text-content ::ng-deep th,.text-content ::ng-deep td{border:var(--text-table-border);padding:var(--text-table-padding);vertical-align:middle;text-align:left}.text-content ::ng-deep th{text-align:center;font-weight:700}.text-content.editing{cursor:text}.text-content.editing ::ng-deep [data-embed-key],.text-content.editing ::ng-deep [data-embed-field]{padding:0 2px;border-radius:3px;background-color:#4e79a729;-webkit-user-select:all;user-select:all}.text-content.editing ::ng-deep [data-embed-key]{cursor:pointer}.text-content.editing ::ng-deep [data-embed-key]:hover{background-color:#4e79a747}.text-content.editing ::ng-deep [data-embed-missing]{background-color:#e1575924;outline:1px dashed rgba(225,87,89,.7)}.text-content.editing ::ng-deep th,.text-content.editing ::ng-deep tfoot td{background-color:transparent}.text-content.editing ::ng-deep .fr-table-selector{display:none}.text-content.editing ::ng-deep .fr-table-selection-hover{outline:none}.text-hint{color:#9aa5b1;-webkit-user-select:none;user-select:none}.value-popover-anchor{position:absolute;pointer-events:none}::ng-deep .value-popover-wrapper .popover-content{padding:15px!important}.value-popover{gap:15px}.value-popover-name{font-weight:600}.value-popover-settings{--mdc-icon-button-state-layer-size: 24px;width:24px;height:24px;padding:0}.value-notice{position:absolute;top:100%;left:0;z-index:2;margin-top:4px;padding:2px 4px 2px 10px;border:1px solid #d9dee3;border-radius:4px;background:#fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif}\n"] }]
|
|
11783
11800
|
}], ctorParameters: () => [] });
|
|
11784
11801
|
|
|
11785
11802
|
// Resize handles, named by compass direction.
|
|
@@ -15182,12 +15199,11 @@ const KPI_EXPORT_MIN_SCALE = 0.5;
|
|
|
15182
15199
|
// failures.
|
|
15183
15200
|
const EXPORT_CONCURRENCY = 3;
|
|
15184
15201
|
// What the export gates call the header and footer: when a value they show
|
|
15185
|
-
// came back without its number, and when one of their
|
|
15186
|
-
//
|
|
15202
|
+
// came back without its number, and when one of their blocks could not be
|
|
15203
|
+
// drawn.
|
|
15187
15204
|
const HEADER_FOOTER_NAME = 'Header & footer';
|
|
15188
|
-
// How much of a text block's words the export gates quote to name
|
|
15189
|
-
|
|
15190
|
-
const OVERFLOW_SNIPPET_LENGTH = 40;
|
|
15205
|
+
// How much of a text block's words the export gates quote to name the block.
|
|
15206
|
+
const TEXT_SNIPPET_LENGTH = 40;
|
|
15191
15207
|
// What a component with nothing to draw prints in the exported file.
|
|
15192
15208
|
//
|
|
15193
15209
|
// The two cases must not read alike. A tile that returned no rows is a finding
|
|
@@ -15221,16 +15237,11 @@ function exportTitle(component, tokens, search, crumbs, filtered = false) {
|
|
|
15221
15237
|
function isFailed(item) {
|
|
15222
15238
|
return !item.data && item.component.type !== 'text';
|
|
15223
15239
|
}
|
|
15224
|
-
// A text block
|
|
15225
|
-
//
|
|
15226
|
-
|
|
15227
|
-
function textOverflow(component, page, tokens) {
|
|
15240
|
+
// A text block as the export gates name it: by the start of its words, on one
|
|
15241
|
+
// line.
|
|
15242
|
+
function textSnippet(component, tokens) {
|
|
15228
15243
|
const text = textPlain(component.config?.html ?? '', tokens).replace(/\s+/g, ' ').trim();
|
|
15229
|
-
return {
|
|
15230
|
-
componentId: component.id,
|
|
15231
|
-
page,
|
|
15232
|
-
snippet: text.length > OVERFLOW_SNIPPET_LENGTH ? `${text.slice(0, OVERFLOW_SNIPPET_LENGTH).trimEnd()}…` : text,
|
|
15233
|
-
};
|
|
15244
|
+
return text.length > TEXT_SNIPPET_LENGTH ? `${text.slice(0, TEXT_SNIPPET_LENGTH).trimEnd()}…` : text;
|
|
15234
15245
|
}
|
|
15235
15246
|
// The lines under the report's name on the PDF title page and the title slide:
|
|
15236
15247
|
// the export date, the active filters, then what the numbers are measured in.
|
|
@@ -15839,7 +15850,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
15839
15850
|
// in the report's format context, as on screen.
|
|
15840
15851
|
async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
|
|
15841
15852
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
15842
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
15853
|
+
const echarts = (await import('./firestitch-report-echarts-CBBTk1Ql.mjs')).default;
|
|
15843
15854
|
const host = document.createElement('div');
|
|
15844
15855
|
host.style.width = `${width}px`;
|
|
15845
15856
|
host.style.height = `${height}px`;
|
|
@@ -15864,17 +15875,12 @@ async function renderChartImage(component, data, width, height, context, pixelRa
|
|
|
15864
15875
|
}
|
|
15865
15876
|
}
|
|
15866
15877
|
|
|
15867
|
-
// How far past the bottom of its box a block may reach and still fit: two
|
|
15868
|
-
// pixels, so the rounding of the last line's edge never counts as overflow.
|
|
15869
|
-
// The deck keeps a table's row on the same terms.
|
|
15870
|
-
const OVERFLOW_SLACK = 2 / PX_PER_INCH;
|
|
15871
15878
|
// Where the canvas puts each part of a text block, for the exports. A
|
|
15872
15879
|
// browser wraps the words and sizes a table's columns by its own layout, which
|
|
15873
15880
|
// neither jsPDF nor PowerPoint reproduces. So the block is rendered offscreen
|
|
15874
15881
|
// with the canvas's own component, at the canvas's 96px per inch and unscaled,
|
|
15875
15882
|
// and the exports place what they draw where it sat: a table at its top, in
|
|
15876
15883
|
// its column widths and row heights, and the words between tables at theirs.
|
|
15877
|
-
// It also tells whether the block runs past the bottom of its box.
|
|
15878
15884
|
//
|
|
15879
15885
|
// Provided beside the export services, so the block renders under the same
|
|
15880
15886
|
// injector the canvas's blocks do.
|
|
@@ -15969,32 +15975,22 @@ function blockBox(component, text) {
|
|
|
15969
15975
|
}
|
|
15970
15976
|
// Split the rendered block at its tables, as textBlocks splits the same html,
|
|
15971
15977
|
// so the nth segment is the nth text part and the nth table the nth table
|
|
15972
|
-
// part.
|
|
15973
|
-
// anything.
|
|
15978
|
+
// part.
|
|
15974
15979
|
function measureBlock(content) {
|
|
15975
15980
|
const box = content.getBoundingClientRect();
|
|
15976
15981
|
const parts = splitAtTables(content);
|
|
15977
15982
|
const segments = [];
|
|
15978
15983
|
const tables = [];
|
|
15979
|
-
let usedHeight = 0;
|
|
15980
15984
|
parts.forEach((part, index) => {
|
|
15981
15985
|
if (Array.isArray(part)) {
|
|
15982
|
-
|
|
15983
|
-
segments.push(segment);
|
|
15984
|
-
usedHeight = segment?.bottom ?? usedHeight;
|
|
15986
|
+
segments.push(measureSegment(drawnNodes(part, index === parts.length - 1), box));
|
|
15985
15987
|
}
|
|
15986
15988
|
else {
|
|
15987
|
-
|
|
15988
|
-
tables.push(table);
|
|
15989
|
-
usedHeight = table.top + table.height;
|
|
15989
|
+
tables.push(measureTable(part, box));
|
|
15990
15990
|
}
|
|
15991
15991
|
});
|
|
15992
|
-
const contentHeight = inches(box.height);
|
|
15993
15992
|
return {
|
|
15994
15993
|
contentWidth: inches(box.width),
|
|
15995
|
-
contentHeight,
|
|
15996
|
-
usedHeight,
|
|
15997
|
-
overflow: usedHeight > contentHeight + OVERFLOW_SLACK,
|
|
15998
15994
|
segments,
|
|
15999
15995
|
tables,
|
|
16000
15996
|
};
|
|
@@ -16003,7 +15999,7 @@ function measureBlock(content) {
|
|
|
16003
15999
|
// blocks draws nothing anywhere. A blank line at the start of a block is
|
|
16004
16000
|
// drawn, on the canvas and by textBlocks alike, but textBlocks drops the blank
|
|
16005
16001
|
// lines at the very end of a block, so they are not measured either: a
|
|
16006
|
-
// trailing empty paragraph never
|
|
16002
|
+
// trailing empty paragraph never adds to the last segment's height.
|
|
16007
16003
|
function drawnNodes(nodes, blockEnd) {
|
|
16008
16004
|
const skipsLast = blockEnd ? isBlank : isVoid;
|
|
16009
16005
|
let start = 0;
|
|
@@ -16168,9 +16164,6 @@ const TEXT_LINE_HEIGHT = 1.25;
|
|
|
16168
16164
|
// How far a list item's text sits from the block's edge, points; the marker
|
|
16169
16165
|
// hangs in that space.
|
|
16170
16166
|
const LIST_INDENT = 16;
|
|
16171
|
-
// What the export's overflow gate says the PDF does with a text block that does
|
|
16172
|
-
// not fit its box.
|
|
16173
|
-
const PDF_OVERFLOW_NOTE = 'The PDF cuts it off at the bottom of its box.';
|
|
16174
16167
|
// How far a table's group label steps in per level, points.
|
|
16175
16168
|
const GRID_INDENT$1 = 8;
|
|
16176
16169
|
// Builds the PDF client-side with jspdf. The report's unit is inches and PDFs
|
|
@@ -16190,14 +16183,12 @@ const GRID_INDENT$1 = 8;
|
|
|
16190
16183
|
// without end trying to place it.
|
|
16191
16184
|
//
|
|
16192
16185
|
// A component that fails to draw is marked in its box and the file goes on
|
|
16193
|
-
// (_guarded); build() hands back the ones that did
|
|
16194
|
-
// cut.
|
|
16186
|
+
// (_guarded); build() hands back the ones that did.
|
|
16195
16187
|
//
|
|
16196
16188
|
// A text block's tables are drawn by autoTable too, from the line its words
|
|
16197
16189
|
// reach, in the column widths the canvas gives them (ReportTextLayoutService).
|
|
16198
|
-
// A text block is cut at the bottom of its box
|
|
16199
|
-
//
|
|
16200
|
-
// can ask about them before it saves.
|
|
16190
|
+
// A text block is never cut at the bottom of its box: what runs past it draws
|
|
16191
|
+
// on below, as on the canvas, down to the page's edge.
|
|
16201
16192
|
class ReportPdfService {
|
|
16202
16193
|
_textLayout = inject(ReportTextLayoutService);
|
|
16203
16194
|
async build(collected) {
|
|
@@ -16220,8 +16211,6 @@ class ReportPdfService {
|
|
|
16220
16211
|
}
|
|
16221
16212
|
}
|
|
16222
16213
|
return {
|
|
16223
|
-
overflows: [...context.textOverflows.values()],
|
|
16224
|
-
overflowNote: PDF_OVERFLOW_NOTE,
|
|
16225
16214
|
undrawn: [...context.undrawn.values()],
|
|
16226
16215
|
save: () => {
|
|
16227
16216
|
context.doc.save(`${this._slug(collected.report.name)}.pdf`);
|
|
@@ -16249,7 +16238,6 @@ class ReportPdfService {
|
|
|
16249
16238
|
masterValues: this._pdfValues(collected.masterValues),
|
|
16250
16239
|
filterFields: collected.filterFields,
|
|
16251
16240
|
masterFilters: layerFilterFields(collected.filterFields, report.filterGroups ?? [], null),
|
|
16252
|
-
textOverflows: new Map(),
|
|
16253
16241
|
undrawn: new Map(),
|
|
16254
16242
|
};
|
|
16255
16243
|
}
|
|
@@ -16267,8 +16255,8 @@ class ReportPdfService {
|
|
|
16267
16255
|
if (item.component.type !== 'text') {
|
|
16268
16256
|
await this._renderComponent(context, collectedPage.page, item, tokens, overflows);
|
|
16269
16257
|
}
|
|
16270
|
-
else
|
|
16271
|
-
this.
|
|
16258
|
+
else {
|
|
16259
|
+
await this._renderText(context, item.component, pageTokens);
|
|
16272
16260
|
}
|
|
16273
16261
|
});
|
|
16274
16262
|
}
|
|
@@ -16299,7 +16287,7 @@ class ReportPdfService {
|
|
|
16299
16287
|
componentId: component.id,
|
|
16300
16288
|
page,
|
|
16301
16289
|
name: component.type === 'text'
|
|
16302
|
-
?
|
|
16290
|
+
? textSnippet(component, tokens)
|
|
16303
16291
|
: exportTitle(component, tokens),
|
|
16304
16292
|
});
|
|
16305
16293
|
}
|
|
@@ -16326,20 +16314,10 @@ class ReportPdfService {
|
|
|
16326
16314
|
const masterTokens = { ...tokens, values: context.masterValues, filters: context.masterFilters };
|
|
16327
16315
|
for (const component of (context.report.master?.components ?? []).filter(isDrawn)) {
|
|
16328
16316
|
await this._guarded(context, component, HEADER_FOOTER_NAME, masterTokens, true, async () => {
|
|
16329
|
-
|
|
16330
|
-
this._recordOverflow(context, component, HEADER_FOOTER_NAME, masterTokens);
|
|
16331
|
-
}
|
|
16317
|
+
await this._renderText(context, component, masterTokens);
|
|
16332
16318
|
});
|
|
16333
16319
|
}
|
|
16334
16320
|
}
|
|
16335
|
-
// Lists a text block that was cut, under the page the export's overflow gate
|
|
16336
|
-
// names it by. A block cut again on a later page (a header or footer) keeps
|
|
16337
|
-
// the first entry.
|
|
16338
|
-
_recordOverflow(context, component, page, tokens) {
|
|
16339
|
-
if (!context.textOverflows.has(component.id)) {
|
|
16340
|
-
context.textOverflows.set(component.id, textOverflow(component, page, tokens));
|
|
16341
|
-
}
|
|
16342
|
-
}
|
|
16343
16321
|
_titlePage(doc, collected, pageW, pageH) {
|
|
16344
16322
|
doc.setFont('helvetica', 'bold');
|
|
16345
16323
|
doc.setFontSize(28);
|
|
@@ -16354,11 +16332,11 @@ class ReportPdfService {
|
|
|
16354
16332
|
// words between its tables are drawn line by line (_drawParagraphs) and each
|
|
16355
16333
|
// table by autoTable from the line the words above it reach
|
|
16356
16334
|
// (_drawTextTable). Only a block with a table is measured, for the edges
|
|
16357
|
-
// and column widths the canvas gives its tables.
|
|
16358
|
-
// the block's height
|
|
16359
|
-
//
|
|
16335
|
+
// and column widths the canvas gives its tables. Words and table rows that
|
|
16336
|
+
// run past the block's height draw on below it, as on the canvas; only the
|
|
16337
|
+
// page's edge stops them.
|
|
16360
16338
|
async _renderText(context, component, tokens) {
|
|
16361
|
-
const box = this._textBox(component);
|
|
16339
|
+
const box = this._textBox(component, context.pageH);
|
|
16362
16340
|
const blocks = textBlocks(component.config?.html ?? '', tokens);
|
|
16363
16341
|
const layout = blocks.some((block) => block.kind === 'table')
|
|
16364
16342
|
? await this._textLayout.measure(component, tokens)
|
|
@@ -16374,23 +16352,22 @@ class ReportPdfService {
|
|
|
16374
16352
|
? this._drawParagraphs(context.doc, block.paragraphs, box, cursorY)
|
|
16375
16353
|
: this._drawTextTable(context, block.table, layout?.tables[tableIndex++] ?? null, box, onMargin ? cursorY : cursorY + TEXT_TABLE_MARGIN);
|
|
16376
16354
|
if (drawn.cut) {
|
|
16377
|
-
return
|
|
16355
|
+
return;
|
|
16378
16356
|
}
|
|
16379
16357
|
cursorY = drawn.cursorY;
|
|
16380
16358
|
onMargin = block.kind === 'table' || (onMargin && !block.paragraphs.length);
|
|
16381
16359
|
}
|
|
16382
|
-
return false;
|
|
16383
16360
|
}
|
|
16384
16361
|
// Where a text block's words go: inside its padding, each edge 1/8" unless
|
|
16385
|
-
// set.
|
|
16386
|
-
_textBox(component) {
|
|
16362
|
+
// set, down to the bottom of the page (pageH).
|
|
16363
|
+
_textBox(component, pageH) {
|
|
16387
16364
|
const padding = component.config?.padding;
|
|
16388
16365
|
const inset = (value) => (value ?? DEFAULT_COMPONENT_PADDING) * PT;
|
|
16389
16366
|
return {
|
|
16390
16367
|
left: component.x * PT + inset(padding?.left),
|
|
16391
16368
|
top: component.y * PT + inset(padding?.top),
|
|
16392
16369
|
width: component.w * PT - inset(padding?.left) - inset(padding?.right),
|
|
16393
|
-
bottom:
|
|
16370
|
+
bottom: pageH,
|
|
16394
16371
|
};
|
|
16395
16372
|
}
|
|
16396
16373
|
// Paragraphs drawn line by line from cursorY down: each one's runs are broken
|
|
@@ -16454,9 +16431,9 @@ class ReportPdfService {
|
|
|
16454
16431
|
// evenly. A row is as tall as its cells' words wrapped as _drawParagraphs
|
|
16455
16432
|
// wraps them, and the words are drawn into each cell once autoTable has
|
|
16456
16433
|
// drawn its grid lines and fill (_drawTextCell), so they keep their runs'
|
|
16457
|
-
// fonts and colors. Only the rows the box
|
|
16458
|
-
//
|
|
16459
|
-
// TEXT_TABLE_MARGIN under it.
|
|
16434
|
+
// fonts and colors. Only the rows that fit above the box's bottom, the
|
|
16435
|
+
// page's edge, are drawn, and a table that loses any, or runs past its page
|
|
16436
|
+
// anyway, is cut. The cursor it leaves is TEXT_TABLE_MARGIN under it.
|
|
16460
16437
|
_drawTextTable(context, table, layout, box, top) {
|
|
16461
16438
|
if (top >= box.bottom) {
|
|
16462
16439
|
return { cursorY: top, cut: true };
|
|
@@ -17345,10 +17322,6 @@ const PPTX_CONSTANT_LINE_SIZE = 1;
|
|
|
17345
17322
|
// What the upper edge of a reference band is called in the data sheet and the
|
|
17346
17323
|
// legend, after the band's own label on its lower edge.
|
|
17347
17324
|
const PPTX_BAND_UPPER_SUFFIX = ' (upper)';
|
|
17348
|
-
// What the export's overflow gate says the deck does with a text block that
|
|
17349
|
-
// does not fit its box.
|
|
17350
|
-
const PPTX_OVERFLOW_NOTE = 'In the PowerPoint it runs past the bottom of its box, and table rows past it are '
|
|
17351
|
-
+ 'left out.';
|
|
17352
17325
|
// The shortest text box a text block's words get between its tables, inches,
|
|
17353
17326
|
// however close the canvas sets their top and bottom.
|
|
17354
17327
|
const TEXT_SEGMENT_MIN_H = 0.05;
|
|
@@ -17399,15 +17372,10 @@ class ReportPptxService {
|
|
|
17399
17372
|
pptx.layout = 'REPORT';
|
|
17400
17373
|
pptx.title = report.name;
|
|
17401
17374
|
this._titleSlide(pptx, collected, pageW, pageH);
|
|
17402
|
-
// The text blocks that do not fit their box, by component id, so a header
|
|
17403
|
-
// or footer block is listed once, not once per slide.
|
|
17404
|
-
const overflows = new Map();
|
|
17405
17375
|
for (const index of collected.pages.keys()) {
|
|
17406
|
-
await this._renderPage(pptx, collected, index
|
|
17376
|
+
await this._renderPage(pptx, collected, index);
|
|
17407
17377
|
}
|
|
17408
17378
|
return {
|
|
17409
|
-
overflows: [...overflows.values()],
|
|
17410
|
-
overflowNote: PPTX_OVERFLOW_NOTE,
|
|
17411
17379
|
// A slide's shapes are laid out by PowerPoint, so a component cannot
|
|
17412
17380
|
// fail to draw here as a PDF table can.
|
|
17413
17381
|
undrawn: [],
|
|
@@ -17420,7 +17388,7 @@ class ReportPptxService {
|
|
|
17420
17388
|
// components sit on top of them exactly as on the canvas. Each layer's text
|
|
17421
17389
|
// fills its value tokens with that layer's values: the master's or the
|
|
17422
17390
|
// page's.
|
|
17423
|
-
async _renderPage(pptx, collected, index
|
|
17391
|
+
async _renderPage(pptx, collected, index) {
|
|
17424
17392
|
const report = collected.report;
|
|
17425
17393
|
const collectedPage = collected.pages[index];
|
|
17426
17394
|
const slide = pptx.addSlide();
|
|
@@ -17428,37 +17396,27 @@ class ReportPptxService {
|
|
|
17428
17396
|
const pageTokens = { ...tokens, values: collectedPage.values };
|
|
17429
17397
|
const timezone = resolveReportTimezone(report);
|
|
17430
17398
|
if (!collectedPage.page.masterHidden) {
|
|
17431
|
-
await this._renderMaster(slide, collected, tokens
|
|
17399
|
+
await this._renderMaster(slide, collected, tokens);
|
|
17432
17400
|
}
|
|
17433
17401
|
for (const item of collectedPage.components) {
|
|
17434
17402
|
if (item.component.type !== 'text') {
|
|
17435
17403
|
this._renderComponent(pptx, slide, item, tokens, timezone, collected.formatContext);
|
|
17436
17404
|
}
|
|
17437
|
-
else
|
|
17438
|
-
this.
|
|
17405
|
+
else {
|
|
17406
|
+
await this._renderText(slide, item.component, pageTokens);
|
|
17439
17407
|
}
|
|
17440
17408
|
}
|
|
17441
17409
|
}
|
|
17442
17410
|
// The header and footer, their value tokens filled with the master's
|
|
17443
17411
|
// values. They read no page filter: its token prints the em dash there.
|
|
17444
|
-
async _renderMaster(slide, collected, tokens
|
|
17412
|
+
async _renderMaster(slide, collected, tokens) {
|
|
17445
17413
|
const masterTokens = {
|
|
17446
17414
|
...tokens,
|
|
17447
17415
|
values: collected.masterValues,
|
|
17448
17416
|
filters: layerFilterFields(collected.filterFields, collected.report.filterGroups ?? [], null),
|
|
17449
17417
|
};
|
|
17450
17418
|
for (const component of (collected.report.master?.components ?? []).filter(isDrawn)) {
|
|
17451
|
-
|
|
17452
|
-
this._recordOverflow(overflows, component, HEADER_FOOTER_NAME, masterTokens);
|
|
17453
|
-
}
|
|
17454
|
-
}
|
|
17455
|
-
}
|
|
17456
|
-
// Lists a text block that does not fit its box, under the page the export's
|
|
17457
|
-
// overflow gate names it by. A block listed on an earlier slide (a header or
|
|
17458
|
-
// footer) keeps that entry.
|
|
17459
|
-
_recordOverflow(overflows, component, page, tokens) {
|
|
17460
|
-
if (!overflows.has(component.id)) {
|
|
17461
|
-
overflows.set(component.id, textOverflow(component, page, tokens));
|
|
17419
|
+
await this._renderText(slide, component, masterTokens);
|
|
17462
17420
|
}
|
|
17463
17421
|
}
|
|
17464
17422
|
_titleSlide(pptx, collected, pageW, pageH) {
|
|
@@ -17480,28 +17438,28 @@ class ReportPptxService {
|
|
|
17480
17438
|
});
|
|
17481
17439
|
}
|
|
17482
17440
|
// A text block as native PowerPoint text, editable in the deck, each run
|
|
17483
|
-
// carrying its own bold/italic/color/size.
|
|
17484
|
-
//
|
|
17485
|
-
//
|
|
17486
|
-
//
|
|
17487
|
-
//
|
|
17441
|
+
// carrying its own bold/italic/color/size. A block without a table is ONE
|
|
17442
|
+
// text box (_addTextBox). A block with tables is measured as the canvas lays
|
|
17443
|
+
// it out (ReportTextLayoutService) and drawn as a stack inside its padding
|
|
17444
|
+
// (_renderTextStack). Nothing is cut: what runs past the bottom of the box
|
|
17445
|
+
// draws on below it, as on the canvas.
|
|
17488
17446
|
async _renderText(slide, component, tokens) {
|
|
17489
|
-
const
|
|
17490
|
-
const blocks = textBlocks(html, tokens);
|
|
17491
|
-
const layout = await this._textLayout.measure(component, tokens);
|
|
17447
|
+
const blocks = textBlocks(component.config?.html ?? '', tokens);
|
|
17492
17448
|
const textCount = blocks.filter((block) => block.kind === 'text').length;
|
|
17493
17449
|
const tableCount = blocks.length - textCount;
|
|
17450
|
+
const layout = tableCount ? await this._textLayout.measure(component, tokens) : null;
|
|
17494
17451
|
// The measure finds a block's parts one for one with textBlocks, unless the
|
|
17495
17452
|
// canvas rendered the html differently. A block whose parts it did not
|
|
17496
17453
|
// find falls back to the one text box, its tables read as lines of text.
|
|
17497
|
-
if (!
|
|
17454
|
+
if (!layout || layout.tables.length !== tableCount || layout.segments.length !== textCount) {
|
|
17498
17455
|
this._addTextBox(slide, component, blockParagraphs(blocks));
|
|
17499
|
-
return
|
|
17456
|
+
return;
|
|
17500
17457
|
}
|
|
17501
|
-
|
|
17458
|
+
this._renderTextStack(slide, component, blocks, layout);
|
|
17502
17459
|
}
|
|
17503
17460
|
// The words as ONE text box over the whole block. Its inset is the block's
|
|
17504
|
-
// padding, so the words land where they sat on screen
|
|
17461
|
+
// padding, so the words land where they sat on screen, and with no autofit
|
|
17462
|
+
// PowerPoint runs what does not fit on past the box's bottom.
|
|
17505
17463
|
_addTextBox(slide, component, paragraphs) {
|
|
17506
17464
|
const runs = this._paragraphRuns(paragraphs);
|
|
17507
17465
|
if (!runs.length) {
|
|
@@ -17521,33 +17479,28 @@ class ReportPptxService {
|
|
|
17521
17479
|
// words before, between and after its tables, and a native table for each
|
|
17522
17480
|
// table, each at the top the canvas gives it. PowerPoint spaces lines a
|
|
17523
17481
|
// little differently from the canvas, so placing every part afresh keeps
|
|
17524
|
-
// that drift from adding up down the block.
|
|
17525
|
-
// left out.
|
|
17482
|
+
// that drift from adding up down the block.
|
|
17526
17483
|
_renderTextStack(slide, component, blocks, layout) {
|
|
17527
17484
|
const padding = component.config?.padding;
|
|
17528
17485
|
const x = component.x + (padding?.left ?? DEFAULT_COMPONENT_PADDING);
|
|
17529
17486
|
const y = component.y + (padding?.top ?? DEFAULT_COMPONENT_PADDING);
|
|
17530
17487
|
let segmentIndex = 0;
|
|
17531
17488
|
let tableIndex = 0;
|
|
17532
|
-
let flagged = false;
|
|
17533
17489
|
for (const block of blocks) {
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
|
|
17490
|
+
if (block.kind === 'text') {
|
|
17491
|
+
this._addTextSegment(slide, block.paragraphs, layout.segments[segmentIndex++], layout, x, y);
|
|
17492
|
+
}
|
|
17493
|
+
else {
|
|
17494
|
+
this._addTextTable(slide, block.table, layout.tables[tableIndex++], x, y);
|
|
17495
|
+
}
|
|
17538
17496
|
}
|
|
17539
|
-
return flagged;
|
|
17540
17497
|
}
|
|
17541
17498
|
// The words between two tables (or before the first, or after the last) as
|
|
17542
17499
|
// a text box from the top of their first line to the bottom of their last,
|
|
17543
|
-
// as wide as the block's content. A stretch that draws nothing is skipped
|
|
17544
|
-
// and one that starts below the box is left out. Returns whether it was.
|
|
17500
|
+
// as wide as the block's content. A stretch that draws nothing is skipped.
|
|
17545
17501
|
_addTextSegment(slide, paragraphs, segment, layout, x, y) {
|
|
17546
17502
|
if (!segment || !paragraphs.some((paragraph) => paragraph.runs.length)) {
|
|
17547
|
-
return
|
|
17548
|
-
}
|
|
17549
|
-
if (segment.top >= layout.contentHeight) {
|
|
17550
|
-
return true;
|
|
17503
|
+
return;
|
|
17551
17504
|
}
|
|
17552
17505
|
slide.addText(this._paragraphRuns(paragraphs), {
|
|
17553
17506
|
x, y: y + segment.top, w: layout.contentWidth, h: Math.max(TEXT_SEGMENT_MIN_H, segment.bottom - segment.top),
|
|
@@ -17556,54 +17509,37 @@ class ReportPptxService {
|
|
|
17556
17509
|
fontFace: 'Arial',
|
|
17557
17510
|
fit: 'none',
|
|
17558
17511
|
});
|
|
17559
|
-
return false;
|
|
17560
17512
|
}
|
|
17561
17513
|
// A table as a native, editable PowerPoint table where the canvas puts it,
|
|
17562
17514
|
// in its column widths and row heights, in the one look every renderer
|
|
17563
|
-
// draws (TEXT_TABLE_* in text.ts).
|
|
17564
|
-
|
|
17565
|
-
|
|
17566
|
-
|
|
17567
|
-
if (kept) {
|
|
17568
|
-
slide.addTable(this._tableRows(table, kept), {
|
|
17569
|
-
x: x + tableLayout.left,
|
|
17570
|
-
y: y + tableLayout.top,
|
|
17571
|
-
w: tableLayout.width,
|
|
17572
|
-
colW: tableLayout.columnWidths,
|
|
17573
|
-
rowH: tableLayout.rowHeights.slice(0, kept),
|
|
17574
|
-
autoPage: false,
|
|
17575
|
-
fontFace: 'Arial',
|
|
17576
|
-
fontSize: DEFAULT_TEXT_SIZE,
|
|
17577
|
-
color: DEFAULT_TEXT_COLOR.replace('#', ''),
|
|
17578
|
-
border: { type: 'solid', pt: TEXT_TABLE_BORDER_WIDTH, color: TEXT_TABLE_BORDER_COLOR.replace('#', '') },
|
|
17579
|
-
margin: TEXT_TABLE_CELL_MARGIN,
|
|
17580
|
-
});
|
|
17515
|
+
// draws (TEXT_TABLE_* in text.ts). Every row is kept.
|
|
17516
|
+
_addTextTable(slide, table, tableLayout, x, y) {
|
|
17517
|
+
if (!table.rows.length) {
|
|
17518
|
+
return;
|
|
17581
17519
|
}
|
|
17582
|
-
|
|
17520
|
+
slide.addTable(this._tableRows(table), {
|
|
17521
|
+
x: x + tableLayout.left,
|
|
17522
|
+
y: y + tableLayout.top,
|
|
17523
|
+
w: tableLayout.width,
|
|
17524
|
+
colW: tableLayout.columnWidths,
|
|
17525
|
+
rowH: tableLayout.rowHeights,
|
|
17526
|
+
autoPage: false,
|
|
17527
|
+
fontFace: 'Arial',
|
|
17528
|
+
fontSize: DEFAULT_TEXT_SIZE,
|
|
17529
|
+
color: DEFAULT_TEXT_COLOR.replace('#', ''),
|
|
17530
|
+
border: { type: 'solid', pt: TEXT_TABLE_BORDER_WIDTH, color: TEXT_TABLE_BORDER_COLOR.replace('#', '') },
|
|
17531
|
+
margin: TEXT_TABLE_CELL_MARGIN,
|
|
17532
|
+
});
|
|
17583
17533
|
}
|
|
17584
|
-
//
|
|
17585
|
-
|
|
17586
|
-
|
|
17587
|
-
|
|
17588
|
-
|
|
17589
|
-
|
|
17590
|
-
|
|
17591
|
-
|
|
17592
|
-
|
|
17593
|
-
kept++;
|
|
17594
|
-
}
|
|
17595
|
-
return kept;
|
|
17596
|
-
}
|
|
17597
|
-
// The kept rows as pptxgenjs cells, each row in grid order. A PowerPoint
|
|
17598
|
-
// table is a full grid, and pptxgenjs counts its columns on the first row
|
|
17599
|
-
// and slips a merged cell's continuation into the row below at that cell's
|
|
17600
|
-
// place in its own row. So a cell is written in its top left slot only, a
|
|
17601
|
-
// slot no cell holds gets an empty cell, and a cell's spans end where they
|
|
17602
|
-
// would reach past the kept rows or into a slot another cell holds
|
|
17603
|
-
// (claimSlots).
|
|
17604
|
-
_tableRows(table, kept) {
|
|
17605
|
-
const slots = Array.from({ length: kept }, () => Array(table.columns).fill(null));
|
|
17606
|
-
table.rows.slice(0, kept).forEach((row) => row.cells.forEach((cell) => claimSlots(slots, cell)));
|
|
17534
|
+
// The rows as pptxgenjs cells, each row in grid order. A PowerPoint table is
|
|
17535
|
+
// a full grid, and pptxgenjs counts its columns on the first row and slips a
|
|
17536
|
+
// merged cell's continuation into the row below at that cell's place in its
|
|
17537
|
+
// own row. So a cell is written in its top left slot only, a slot no cell
|
|
17538
|
+
// holds gets an empty cell, and a cell's spans end where they would reach
|
|
17539
|
+
// past the last row or into a slot another cell holds (claimSlots).
|
|
17540
|
+
_tableRows(table) {
|
|
17541
|
+
const slots = Array.from({ length: table.rows.length }, () => Array(table.columns).fill(null));
|
|
17542
|
+
table.rows.forEach((row) => row.cells.forEach((cell) => claimSlots(slots, cell)));
|
|
17607
17543
|
return slots.map((row, rowIndex) => row.flatMap((cell, column) => {
|
|
17608
17544
|
if (!cell) {
|
|
17609
17545
|
return [{ text: '' }];
|
|
@@ -18564,9 +18500,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
18564
18500
|
// Gives a text table's cell the grid slots it covers from its top left: as
|
|
18565
18501
|
// many columns as are free in its top row, and as many rows down as are free
|
|
18566
18502
|
// across those columns, at most its own spans. A slot is free until a cell
|
|
18567
|
-
// placed before it holds it
|
|
18568
|
-
//
|
|
18569
|
-
//
|
|
18503
|
+
// placed before it holds it. So a span the html takes past the last row ends
|
|
18504
|
+
// there, and one it makes overlap another cell (which a browser draws on top
|
|
18505
|
+
// of it) ends where they meet.
|
|
18570
18506
|
function claimSlots(slots, cell) {
|
|
18571
18507
|
const free = (row, column) => row < slots.length && column < slots[row].length && !slots[row][column];
|
|
18572
18508
|
const columns = (count) => Array.from({ length: count }, (_, offset) => cell.column + offset);
|
|
@@ -18592,8 +18528,7 @@ const RETRY = 'retry';
|
|
|
18592
18528
|
const DOWNLOAD = 'download';
|
|
18593
18529
|
// Runs a report export end to end: collect every tile and value, refuse to
|
|
18594
18530
|
// write a file that is missing any of them without saying so, build it, refuse
|
|
18595
|
-
// to save one that could not draw a component
|
|
18596
|
-
// boxes, without saying so, then save.
|
|
18531
|
+
// to save one that could not draw a component without saying so, then save.
|
|
18597
18532
|
//
|
|
18598
18533
|
// The gates are the reason this sits between the collector and the renderers
|
|
18599
18534
|
// rather than in either of them. A deck with five blank pages is worse than no
|
|
@@ -18604,9 +18539,9 @@ const DOWNLOAD = 'download';
|
|
|
18604
18539
|
// this is the only place that sees it.
|
|
18605
18540
|
//
|
|
18606
18541
|
// Failures are decided before the build, which is the expensive part (every
|
|
18607
|
-
// chart drawn offscreen, every text
|
|
18608
|
-
// Cancel pays for one.
|
|
18609
|
-
// the build is what
|
|
18542
|
+
// chart drawn offscreen, every text table measured), so neither a Retry nor a
|
|
18543
|
+
// Cancel pays for one. A component that could not be drawn is known only after
|
|
18544
|
+
// it: the build is what draws it.
|
|
18610
18545
|
class ReportExportService {
|
|
18611
18546
|
_collector = inject(ReportExportCollectorService);
|
|
18612
18547
|
_pdfService = inject(ReportPdfService);
|
|
@@ -18632,12 +18567,11 @@ class ReportExportService {
|
|
|
18632
18567
|
? this._run(report, listRows, build)
|
|
18633
18568
|
: this._buildAndSave(collected, build)))));
|
|
18634
18569
|
}
|
|
18635
|
-
// Builds the file, then saves it once the undrawn
|
|
18636
|
-
// through.
|
|
18570
|
+
// Builds the file, then saves it once the undrawn gate lets it through.
|
|
18637
18571
|
_buildAndSave(collected, build) {
|
|
18638
18572
|
return from(build(collected))
|
|
18639
18573
|
.pipe(switchMap((built) => this._undrawnGate(built)
|
|
18640
|
-
.pipe(switchMap(() =>
|
|
18574
|
+
.pipe(switchMap(() => from(Promise.resolve(built.save()))))));
|
|
18641
18575
|
}
|
|
18642
18576
|
// Resolves to DOWNLOAD when the export is whole (or the user accepted an
|
|
18643
18577
|
// incomplete one) and to RETRY when they want another attempt. Cancelling
|
|
@@ -18687,31 +18621,6 @@ class ReportExportService {
|
|
|
18687
18621
|
})
|
|
18688
18622
|
.pipe(catchError(() => EMPTY));
|
|
18689
18623
|
}
|
|
18690
|
-
// Resolves to DOWNLOAD when every text block fits its box in the built file,
|
|
18691
|
-
// or the user accepted one that does not. There is no Retry: the words are
|
|
18692
|
-
// the report's own, so a second attempt would build the same file. Each block
|
|
18693
|
-
// is named by its page and the start of its words, and the note says what
|
|
18694
|
-
// this format does with the rest. Cancelling ends the export quietly, as at
|
|
18695
|
-
// the failure gate, and the built file is dropped unsaved.
|
|
18696
|
-
_overflowGate(built) {
|
|
18697
|
-
if (!built.overflows.length) {
|
|
18698
|
-
return of(DOWNLOAD);
|
|
18699
|
-
}
|
|
18700
|
-
// A block with no words to quote (an empty table, say) is named by its
|
|
18701
|
-
// page alone, not by an empty pair of quotes.
|
|
18702
|
-
const blocks = built.overflows
|
|
18703
|
-
.map((overflow) => `<strong>${escapeHtml(overflow.page)}</strong>`
|
|
18704
|
-
+ (overflow.snippet ? ` (“${escapeHtml(overflow.snippet)}”)` : ''));
|
|
18705
|
-
return this._prompt.confirm({
|
|
18706
|
-
title: 'Text does not fit',
|
|
18707
|
-
template: `Some text does not fit its box: ${blocks.join(', ')}. ${built.overflowNote}`,
|
|
18708
|
-
buttons: [
|
|
18709
|
-
{ label: 'Download Anyway', value: DOWNLOAD, color: 'primary' },
|
|
18710
|
-
{ label: 'Cancel', cancel: true, color: '' },
|
|
18711
|
-
],
|
|
18712
|
-
})
|
|
18713
|
-
.pipe(catchError(() => EMPTY));
|
|
18714
|
-
}
|
|
18715
18624
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportExportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
18716
18625
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportExportService });
|
|
18717
18626
|
}
|
|
@@ -19532,7 +19441,7 @@ class FsAiReportComponent {
|
|
|
19532
19441
|
ReportAiChatData,
|
|
19533
19442
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
19534
19443
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
19535
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19444
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CBBTk1Ql.mjs').then((module) => module.default) }),
|
|
19536
19445
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
19537
19446
|
// presets; a preset only sets the days.
|
|
19538
19447
|
provideReportDatePicker(),
|
|
@@ -19556,7 +19465,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19556
19465
|
ReportAiChatData,
|
|
19557
19466
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
19558
19467
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
19559
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19468
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CBBTk1Ql.mjs').then((module) => module.default) }),
|
|
19560
19469
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
19561
19470
|
// presets; a preset only sets the days.
|
|
19562
19471
|
provideReportDatePicker(),
|
|
@@ -19649,4 +19558,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19649
19558
|
*/
|
|
19650
19559
|
|
|
19651
19560
|
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 };
|
|
19652
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
19561
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-Cl_JOBli.mjs.map
|