@firestitch/report 18.17.1 → 18.17.3
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/component-text/component-text.component.d.ts +3 -1
- package/app/reports/data/report.data.d.ts +0 -2
- package/app/reports/export/report-export-collector.service.d.ts +6 -0
- package/app/reports/export/report-export.service.d.ts +1 -0
- package/app/reports/export/report-pdf.service.d.ts +5 -0
- package/esm2022/app/reports/components/component-text/component-text.component.mjs +11 -3
- package/esm2022/app/reports/components/report-component/report-component.component.mjs +3 -3
- package/esm2022/app/reports/data/report.data.mjs +3 -3
- package/esm2022/app/reports/export/report-export-collector.service.mjs +1 -1
- package/esm2022/app/reports/export/report-export.service.mjs +31 -5
- package/esm2022/app/reports/export/report-pdf.service.mjs +212 -80
- package/esm2022/app/reports/export/report-pptx.service.mjs +4 -1
- package/fesm2022/{firestitch-report-echarts-rzMA7zhn.mjs → firestitch-report-echarts-z76sZHp7.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-rzMA7zhn.mjs.map → firestitch-report-echarts-z76sZHp7.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-DRkNBMic.mjs → firestitch-report-firestitch-report-DYs6aHtD.mjs} +262 -93
- package/fesm2022/firestitch-report-firestitch-report-DYs6aHtD.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/firestitch-report-firestitch-report-DRkNBMic.mjs.map +0 -1
|
@@ -429,14 +429,14 @@ class ReportData {
|
|
|
429
429
|
}
|
|
430
430
|
// Make one column filterable. `level` (component | report) places a new
|
|
431
431
|
// group; `groupId` instead links into an existing group. A select filter's
|
|
432
|
-
// options are derived from the component
|
|
432
|
+
// options are derived from the component's own query.
|
|
433
433
|
addFilter(reportId, componentId, filter, config = {}) {
|
|
434
434
|
return this._api.post(this._path(`${reportId}/components/${componentId}/filters`), filter, {
|
|
435
435
|
key: 'filter',
|
|
436
436
|
...config,
|
|
437
437
|
});
|
|
438
438
|
}
|
|
439
|
-
// Update a filter — any subset of { label,
|
|
439
|
+
// Update a filter — any subset of { label, enabled }.
|
|
440
440
|
updateFilter(reportId, filterId, changes, config = {}) {
|
|
441
441
|
return this._api.put(this._path(`${reportId}/filters/${filterId}`), changes, {
|
|
442
442
|
key: 'filter',
|
|
@@ -11211,6 +11211,8 @@ class ComponentTextComponent {
|
|
|
11211
11211
|
html = input('');
|
|
11212
11212
|
tokens = input.required();
|
|
11213
11213
|
editing = input(false);
|
|
11214
|
+
// The canvas is being laid out, so an empty block shows how to fill it.
|
|
11215
|
+
editMode = input(false);
|
|
11214
11216
|
// The value keys the other text blocks of this block's layer show. Asked
|
|
11215
11217
|
// for when an edit takes the last chip of a value out of this block, as a
|
|
11216
11218
|
// value another block still shows is not left unused; a function, as a
|
|
@@ -11323,6 +11325,12 @@ class ComponentTextComponent {
|
|
|
11323
11325
|
},
|
|
11324
11326
|
};
|
|
11325
11327
|
displayHtml = computed(() => this._sanitizer.bypassSecurityTrustHtml(fillTextTokens(this.html(), this.tokens())));
|
|
11328
|
+
// The block shows nothing: no words and no table. A new block is stored
|
|
11329
|
+
// empty, and an emptied editor can still leave a bare <p><br></p>.
|
|
11330
|
+
isBlank = computed(() => {
|
|
11331
|
+
const body = new DOMParser().parseFromString(this.html(), 'text/html').body;
|
|
11332
|
+
return !body.textContent?.trim() && !body.querySelector('table');
|
|
11333
|
+
});
|
|
11326
11334
|
// What the editor holds right now.
|
|
11327
11335
|
draft = '';
|
|
11328
11336
|
_editor = viewChild(FsHtmlEditorComponent);
|
|
@@ -11493,7 +11501,7 @@ class ComponentTextComponent {
|
|
|
11493
11501
|
}
|
|
11494
11502
|
}
|
|
11495
11503
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11496
|
-
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 }, 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 {\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:hidden;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{overflow:auto;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}.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 });
|
|
11504
|
+
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:hidden;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{overflow:auto;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 });
|
|
11497
11505
|
}
|
|
11498
11506
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentTextComponent, decorators: [{
|
|
11499
11507
|
type: Component,
|
|
@@ -11518,7 +11526,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
11518
11526
|
'[style.--text-table-border]': 'tableLook.border',
|
|
11519
11527
|
'[style.--text-table-padding]': 'tableLook.padding',
|
|
11520
11528
|
'[style.--text-table-margin]': 'tableLook.margin',
|
|
11521
|
-
}, 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 {\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:hidden;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{overflow:auto;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}.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"] }]
|
|
11529
|
+
}, 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:hidden;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{overflow:auto;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"] }]
|
|
11522
11530
|
}], ctorParameters: () => [] });
|
|
11523
11531
|
|
|
11524
11532
|
// Resize handles, named by compass direction.
|
|
@@ -12252,7 +12260,7 @@ class ReportComponentComponent {
|
|
|
12252
12260
|
openAtPoint(anchor, click.pointer, this.zoom, () => this.markMenu?.openMenu(null));
|
|
12253
12261
|
}
|
|
12254
12262
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12255
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportComponentComponent, isStandalone: true, selector: "app-report-component", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: false, isRequired: false, transformFunction: null }, component: { classPropertyName: "component", publicName: "component", isSignal: false, isRequired: false, transformFunction: null }, groups: { classPropertyName: "groups", publicName: "groups", isSignal: false, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: false, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: false, isRequired: false, transformFunction: null }, snapper: { classPropertyName: "snapper", publicName: "snapper", isSignal: false, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: false, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: false, transformFunction: null }, valueKeysElsewhere: { classPropertyName: "valueKeysElsewhere", publicName: "valueKeysElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectComponent: "selectComponent", positionChanged: "positionChanged", openSettings: "openSettings", removed: "removed", valueSettings: "valueSettings", textSaved: "textSaved", valuesRemoved: "valuesRemoved" }, host: { properties: { "class.height-auto": "this.heightAuto" } }, viewQueries: [{ propertyName: "_text", first: true, predicate: ComponentTextComponent, descendants: true, isSignal: true }, { propertyName: "chartComponent", first: true, predicate: ComponentChartComponent, descendants: true }, { propertyName: "markMenu", first: true, predicate: ["markMenu"], descendants: true }, { propertyName: "markAnchor", first: true, predicate: ["markAnchor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"component-box\"\n [class.editing]=\"editMode\"\n [class.text]=\"isText\"\n [class.selected]=\"selected\"\n [class.refreshing]=\"refreshing()\"\n [style.padding]=\"bodyPadding\"\n (pointerdown)=\"onComponentPointerDown($event)\"\n (mousedown)=\"stopCanvasPan($event)\"\n (touchstart)=\"stopCanvasPan($event)\">\n <!-- The header is an fs-filter: heading template = title (and the edit-mode\n drag handle), actions = the menu, items = component-level filter groups,\n the same strip for every type (lists included). fs-filter reads its\n config once, so the block is keyed on the component-filter set to be\n recreated when it changes. The title shows its tokens filled, and a\n filter change repaints it in place (shownTitle). -->\n <!-- A text block has no title bar \u2014 it IS its words, like a text box on a\n slide \u2014 so it gets neither the header nor the data actions. -->\n @for (key of isText ? [] : [filterKey]; track key) {\n <fs-filter\n class=\"component-filter\"\n [config]=\"filterConfig()\">\n <ng-template fsFilterHeading>\n <div\n class=\"component-title\"\n (pointerdown)=\"onDragStart($event)\">\n <!-- While the chart is drilled the title leads its breadcrumb:\n 'Title \u203A 2026 \u203A Q3', the title as the viewer reads it\n (shownTitle). The title goes back to the top and each\n crumb to the period it names; the last is where the chart is.\n A long path is cut at its left end, so the crumbs nearest the\n chart stay in view. While the layout is edited the crumbs are\n plain text (the stylesheet; drillTo does nothing then). -->\n @if (drillCrumbs().length) {\n <span class=\"drill-path\">\n <span dir=\"ltr\">\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo(0)\"\n (keydown.enter)=\"drillTo(0)\">\n {{ shownTitle }}\n </a>\n @for (label of drillCrumbs(); track $index; let last = $last) {\n <span class=\"drill-separator\">{{ drillSeparator }}</span>\n @if (last) {\n <span>{{ label }}</span>\n } @else {\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo($index + 1)\"\n (keydown.enter)=\"drillTo($index + 1)\">\n {{ label }}\n </a>\n }\n }\n </span>\n </span>\n } @else {\n {{ shownTitle }}\n }\n @if (truncated) {\n <mat-icon\n class=\"truncated-icon\"\n matTooltip=\"Showing a truncated result \u2014 refine the filters to see everything.\">\n warning_amber\n </mat-icon>\n }\n </div>\n </ng-template>\n </fs-filter>\n }\n <!-- The actions menu is floated top-right (absolute) rather than living in\n fs-filter's actions slot, so its ~40px button height no longer drives the\n header row's height. It overlays the box corner; the heading reserves\n right padding so a long title ellipsises before it. -->\n @if (!isText || (editMode && !textEditing())) {\n <button\n mat-icon-button\n class=\"component-menu\"\n [fsMenuTriggerFor]=\"menu\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"stopCanvasPan($event)\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <!-- Beside its trigger, inside the same block: an fs-menu that finds no\n trigger of its own draws one. -->\n <fs-menu #menu>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"editText()\">\n <mat-icon>edit</mat-icon>\n <span>Edit text</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"deleteText()\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillUp()\">\n <mat-icon>arrow_upward</mat-icon>\n <span>Drill up</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillTo(0)\">\n <mat-icon>vertical_align_top</mat-icon>\n <span>Back to top</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"settings()\">\n <mat-icon>tune</mat-icon>\n <span>Settings</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportCsv()\">\n <mat-icon>download</mat-icon>\n <span>Export CSV</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportExcel()\">\n <mat-icon>table_view</mat-icon>\n <span>Export Excel</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!hidesColumns\"\n (click)=\"chooseColumns()\">\n <mat-icon>view_column</mat-icon>\n <span>Columns</span>\n </ng-template>\n </fs-menu>\n }\n <div class=\"component-body\">\n @if (isText) {\n <app-report-component-text\n [html]=\"textHtml() ?? component.config?.html ?? ''\"\n [tokens]=\"tokens()\"\n [editing]=\"textEditing()\"\n [shownElsewhere]=\"shownElsewhere\"\n (saved)=\"onTextSaved($event)\"\n (editEnded)=\"textEditing.set(false)\"\n (valueSettings)=\"valueSettings.emit($event)\">\n </app-report-component-text>\n } @else if (component.type === 'list') {\n @for (key of [listResetKey()]; track key) {\n <app-report-component-list\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-list>\n }\n } @else if (component.type === 'grid') {\n <app-report-component-grid\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-grid>\n } @else if (loading()) {\n <!-- Holds the tile's height for the spinner on the first load. -->\n <div class=\"component-state\"></div>\n } @else if (error()) {\n <div class=\"component-state error\">\n {{ error() }}\n </div>\n } @else if (!data()?.rows?.length) {\n <div class=\"component-state\">\n {{ emptyMessage() }}\n </div>\n } @else if (component.type === 'kpi') {\n <app-report-component-kpi\n [component]=\"component\"\n [data]=\"data()\"\n (markClick)=\"onMarkClick($event)\">\n </app-report-component-kpi>\n } @else {\n <app-report-component-chart\n [component]=\"component\"\n [data]=\"data()\"\n [actions]=\"tileActions()\"\n [(legendSelected)]=\"legendSelected\"\n (markClick)=\"onMarkClick($event)\"\n (markMiss)=\"onMarkMiss()\">\n </app-report-component-chart>\n }\n <!-- In edit mode a transparent veil over the body makes the WHOLE\n component draggable (industry standard: you grab the object, not just\n its title bar) and keeps inner widgets from swallowing the gesture. -->\n @if (editMode && !textEditing()) {\n <div\n class=\"drag-veil\"\n (pointerdown)=\"onDragStart($event)\">\n </div>\n }\n </div>\n <!-- A load: a spinner centred on the whole tile. On a refetch the body and\n the menu are dimmed under it (.refreshing); the title is not. -->\n @if (loading() || refreshing()) {\n <mat-progress-spinner\n class=\"load-spinner\"\n aria-label=\"Loading data\"\n mode=\"indeterminate\"\n [diameter]=\"15\">\n </mat-progress-spinner>\n }\n</div>\n<!-- The mark menu, for a click whose mark offers two or more actions. It\n opens at the click: its trigger is a zero-size anchor that\n anchor-at-point.ts moves there first. -->\n@if (!isText && !editMode) {\n <span\n #markAnchor\n [style.position]=\"'absolute'\"\n [fsMenuTriggerFor]=\"markMenu\">\n </span>\n <fs-menu #markMenu>\n @for (action of markMenuActions(); track action) {\n <ng-template\n fs-menu-item\n (click)=\"runMarkAction(action)\">\n {{ markActionLabels[action] }}\n </ng-template>\n }\n </fs-menu>\n}\n<!-- Handles live OUTSIDE the clipped box so they straddle its edges. -->\n@if (editMode && selected) {\n @for (handle of resizeHandles; track handle) {\n <div\n class=\"handle handle-{{ handle }}\"\n (mousedown)=\"stopCanvasPan($event)\"\n (pointerdown)=\"onResizeStart($event, handle)\">\n </div>\n }\n}", styles: [":host{position:absolute;display:flex;flex-direction:column;font-size:10px;--mdc-outlined-button-container-height: 30px;--mdc-outlined-button-label-text-size: 10px;--mat-outlined-button-horizontal-padding: 15px;--mdc-text-button-container-height: 30px;--mdc-text-button-label-text-size: 10px;--mat-text-button-horizontal-padding: 15px;--mat-button-outlined-container-height: 30px;--mat-button-outlined-label-text-size: 10px;--mat-button-outlined-horizontal-padding: 15px;--mat-button-text-container-height: 30px;--mat-button-text-label-text-size: 10px;--mat-button-text-horizontal-padding: 15px}.component-box{position:relative;width:100%;flex:1 1 auto;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;background:#fff;border:1px solid #e4e7eb;border-radius:6px;overflow:hidden;transition:box-shadow .12s ease,border-color .12s ease;cursor:default}.component-box.editing .component-title{cursor:grab}.component-box.editing .component-title:active{cursor:grabbing}.component-box.editing .drill-crumb{pointer-events:none}.component-box.editing:hover{border-color:#9db3c8;box-shadow:0 2px 8px #0f172a14}.component-box.selected{border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color),0 4px 14px #2196f32e}.component-box.text{background:transparent;border-color:transparent;border-radius:0;overflow:visible}.component-box.text.editing{border-style:dashed;border-color:#cbd2d9}.component-box.text.editing:hover{box-shadow:none}.component-box.text.selected{border-style:solid;border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color)}:host ::ng-deep .component-filter .filter-bar-container{align-items:center!important}.component-filter{flex:0 0 auto;display:block;padding:0 0 4px}.component-filter .component-title{display:flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;padding-right:40px;font-size:var(--report-heading-size);font-weight:600;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.component-filter .component-title .truncated-icon{font-size:16px;width:16px;height:16px;color:#f59e0b}.component-filter .component-title .drill-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.component-filter .component-title .drill-crumb{color:inherit;cursor:pointer;text-decoration:none}.component-filter .component-title .drill-crumb:hover,.component-filter .component-title .drill-crumb:focus-visible{text-decoration:underline}.component-filter .component-title .drill-separator{opacity:.6}.component-menu{position:absolute;top:2px;right:2px;z-index:30}.component-body{flex:1 1 auto;min-height:0;position:relative}.component-box.refreshing .component-body,.component-box.refreshing .component-menu{opacity:.35;transition:opacity .15s ease}.load-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));z-index:25;pointer-events:none}:host(.height-auto) .component-state{min-height:120px}.drag-veil{position:absolute;inset:0;cursor:grab;touch-action:none}.drag-veil:active{cursor:grabbing}.component-state{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:12px;color:#7b8794;padding:8px;text-align:center}.component-state.error{color:#e15759}.handle{position:absolute;width:14px;height:14px;display:flex;align-items:center;justify-content:center;touch-action:none;z-index:20;transform:scale(calc(1 / var(--canvas-zoom, 1)))}.handle:before{content:\"\";width:8px;height:8px;background:#fff;border:1.5px solid var(--brand-primary-color);border-radius:2px;box-shadow:0 1px 2px #0f172a33}.handle-nw{left:-7px;top:-7px;cursor:nwse-resize}.handle-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}.handle-ne{right:-7px;top:-7px;cursor:nesw-resize}.handle-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}.handle-se{right:-7px;bottom:-7px;cursor:nwse-resize}.handle-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}.handle-sw{left:-7px;bottom:-7px;cursor:nesw-resize}.handle-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}\n"], dependencies: [{ kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { 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: FsFilterModule }, { kind: "component", type: i1$3.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "directive", type: i1$3.FilterHeadingDirective, selector: "[fsFilterHeading]" }, { 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: ComponentChartComponent, selector: "app-report-component-chart", inputs: ["component", "data", "legendSelected", "actions"], outputs: ["legendSelectedChange", "markClick", "markMiss"] }, { kind: "component", type: ComponentGridComponent, selector: "app-report-component-grid", inputs: ["reportId", "component"] }, { kind: "component", type: ComponentKpiComponent, selector: "app-report-component-kpi", inputs: ["component", "data"], outputs: ["markClick"] }, { kind: "component", type: ComponentListComponent, selector: "app-report-component-list", inputs: ["reportId", "component"] }, { kind: "component", type: ComponentTextComponent, selector: "app-report-component-text", inputs: ["html", "tokens", "editing", "shownElsewhere"], outputs: ["saved", "editEnded", "valueSettings"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12263
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportComponentComponent, isStandalone: true, selector: "app-report-component", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: false, isRequired: false, transformFunction: null }, component: { classPropertyName: "component", publicName: "component", isSignal: false, isRequired: false, transformFunction: null }, groups: { classPropertyName: "groups", publicName: "groups", isSignal: false, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: false, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: false, isRequired: false, transformFunction: null }, snapper: { classPropertyName: "snapper", publicName: "snapper", isSignal: false, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: false, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: false, transformFunction: null }, valueKeysElsewhere: { classPropertyName: "valueKeysElsewhere", publicName: "valueKeysElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectComponent: "selectComponent", positionChanged: "positionChanged", openSettings: "openSettings", removed: "removed", valueSettings: "valueSettings", textSaved: "textSaved", valuesRemoved: "valuesRemoved" }, host: { properties: { "class.height-auto": "this.heightAuto" } }, viewQueries: [{ propertyName: "_text", first: true, predicate: ComponentTextComponent, descendants: true, isSignal: true }, { propertyName: "chartComponent", first: true, predicate: ComponentChartComponent, descendants: true }, { propertyName: "markMenu", first: true, predicate: ["markMenu"], descendants: true }, { propertyName: "markAnchor", first: true, predicate: ["markAnchor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"component-box\"\n [class.editing]=\"editMode\"\n [class.text]=\"isText\"\n [class.selected]=\"selected\"\n [class.refreshing]=\"refreshing()\"\n [style.padding]=\"bodyPadding\"\n (pointerdown)=\"onComponentPointerDown($event)\"\n (mousedown)=\"stopCanvasPan($event)\"\n (touchstart)=\"stopCanvasPan($event)\">\n <!-- The header is an fs-filter: heading template = title (and the edit-mode\n drag handle), actions = the menu, items = component-level filter groups,\n the same strip for every type (lists included). fs-filter reads its\n config once, so the block is keyed on the component-filter set to be\n recreated when it changes. The title shows its tokens filled, and a\n filter change repaints it in place (shownTitle). -->\n <!-- A text block has no title bar \u2014 it IS its words, like a text box on a\n slide \u2014 so it gets neither the header nor the data actions. -->\n @for (key of isText ? [] : [filterKey]; track key) {\n <fs-filter\n class=\"component-filter\"\n [config]=\"filterConfig()\">\n <ng-template fsFilterHeading>\n <div\n class=\"component-title\"\n (pointerdown)=\"onDragStart($event)\">\n <!-- While the chart is drilled the title leads its breadcrumb:\n 'Title \u203A 2026 \u203A Q3', the title as the viewer reads it\n (shownTitle). The title goes back to the top and each\n crumb to the period it names; the last is where the chart is.\n A long path is cut at its left end, so the crumbs nearest the\n chart stay in view. While the layout is edited the crumbs are\n plain text (the stylesheet; drillTo does nothing then). -->\n @if (drillCrumbs().length) {\n <span class=\"drill-path\">\n <span dir=\"ltr\">\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo(0)\"\n (keydown.enter)=\"drillTo(0)\">\n {{ shownTitle }}\n </a>\n @for (label of drillCrumbs(); track $index; let last = $last) {\n <span class=\"drill-separator\">{{ drillSeparator }}</span>\n @if (last) {\n <span>{{ label }}</span>\n } @else {\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo($index + 1)\"\n (keydown.enter)=\"drillTo($index + 1)\">\n {{ label }}\n </a>\n }\n }\n </span>\n </span>\n } @else {\n {{ shownTitle }}\n }\n @if (truncated) {\n <mat-icon\n class=\"truncated-icon\"\n matTooltip=\"Showing a truncated result \u2014 refine the filters to see everything.\">\n warning_amber\n </mat-icon>\n }\n </div>\n </ng-template>\n </fs-filter>\n }\n <!-- The actions menu is floated top-right (absolute) rather than living in\n fs-filter's actions slot, so its ~40px button height no longer drives the\n header row's height. It overlays the box corner; the heading reserves\n right padding so a long title ellipsises before it. -->\n @if (!isText || (editMode && !textEditing())) {\n <button\n mat-icon-button\n class=\"component-menu\"\n [fsMenuTriggerFor]=\"menu\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"stopCanvasPan($event)\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <!-- Beside its trigger, inside the same block: an fs-menu that finds no\n trigger of its own draws one. -->\n <fs-menu #menu>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"editText()\">\n <mat-icon>edit</mat-icon>\n <span>Edit text</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"deleteText()\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillUp()\">\n <mat-icon>arrow_upward</mat-icon>\n <span>Drill up</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillTo(0)\">\n <mat-icon>vertical_align_top</mat-icon>\n <span>Back to top</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"settings()\">\n <mat-icon>tune</mat-icon>\n <span>Settings</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportCsv()\">\n <mat-icon>download</mat-icon>\n <span>Export CSV</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportExcel()\">\n <mat-icon>table_view</mat-icon>\n <span>Export Excel</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!hidesColumns\"\n (click)=\"chooseColumns()\">\n <mat-icon>view_column</mat-icon>\n <span>Columns</span>\n </ng-template>\n </fs-menu>\n }\n <div class=\"component-body\">\n @if (isText) {\n <app-report-component-text\n [html]=\"textHtml() ?? component.config?.html ?? ''\"\n [tokens]=\"tokens()\"\n [editing]=\"textEditing()\"\n [editMode]=\"editMode\"\n [shownElsewhere]=\"shownElsewhere\"\n (saved)=\"onTextSaved($event)\"\n (editEnded)=\"textEditing.set(false)\"\n (valueSettings)=\"valueSettings.emit($event)\">\n </app-report-component-text>\n } @else if (component.type === 'list') {\n @for (key of [listResetKey()]; track key) {\n <app-report-component-list\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-list>\n }\n } @else if (component.type === 'grid') {\n <app-report-component-grid\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-grid>\n } @else if (loading()) {\n <!-- Holds the tile's height for the spinner on the first load. -->\n <div class=\"component-state\"></div>\n } @else if (error()) {\n <div class=\"component-state error\">\n {{ error() }}\n </div>\n } @else if (!data()?.rows?.length) {\n <div class=\"component-state\">\n {{ emptyMessage() }}\n </div>\n } @else if (component.type === 'kpi') {\n <app-report-component-kpi\n [component]=\"component\"\n [data]=\"data()\"\n (markClick)=\"onMarkClick($event)\">\n </app-report-component-kpi>\n } @else {\n <app-report-component-chart\n [component]=\"component\"\n [data]=\"data()\"\n [actions]=\"tileActions()\"\n [(legendSelected)]=\"legendSelected\"\n (markClick)=\"onMarkClick($event)\"\n (markMiss)=\"onMarkMiss()\">\n </app-report-component-chart>\n }\n <!-- In edit mode a transparent veil over the body makes the WHOLE\n component draggable (industry standard: you grab the object, not just\n its title bar) and keeps inner widgets from swallowing the gesture. -->\n @if (editMode && !textEditing()) {\n <div\n class=\"drag-veil\"\n (pointerdown)=\"onDragStart($event)\">\n </div>\n }\n </div>\n <!-- A load: a spinner centred on the whole tile. On a refetch the body and\n the menu are dimmed under it (.refreshing); the title is not. -->\n @if (loading() || refreshing()) {\n <mat-progress-spinner\n class=\"load-spinner\"\n aria-label=\"Loading data\"\n mode=\"indeterminate\"\n [diameter]=\"15\">\n </mat-progress-spinner>\n }\n</div>\n<!-- The mark menu, for a click whose mark offers two or more actions. It\n opens at the click: its trigger is a zero-size anchor that\n anchor-at-point.ts moves there first. -->\n@if (!isText && !editMode) {\n <span\n #markAnchor\n [style.position]=\"'absolute'\"\n [fsMenuTriggerFor]=\"markMenu\">\n </span>\n <fs-menu #markMenu>\n @for (action of markMenuActions(); track action) {\n <ng-template\n fs-menu-item\n (click)=\"runMarkAction(action)\">\n {{ markActionLabels[action] }}\n </ng-template>\n }\n </fs-menu>\n}\n<!-- Handles live OUTSIDE the clipped box so they straddle its edges. -->\n@if (editMode && selected) {\n @for (handle of resizeHandles; track handle) {\n <div\n class=\"handle handle-{{ handle }}\"\n (mousedown)=\"stopCanvasPan($event)\"\n (pointerdown)=\"onResizeStart($event, handle)\">\n </div>\n }\n}", styles: [":host{position:absolute;display:flex;flex-direction:column;font-size:10px;--mdc-outlined-button-container-height: 30px;--mdc-outlined-button-label-text-size: 10px;--mat-outlined-button-horizontal-padding: 15px;--mdc-text-button-container-height: 30px;--mdc-text-button-label-text-size: 10px;--mat-text-button-horizontal-padding: 15px;--mat-button-outlined-container-height: 30px;--mat-button-outlined-label-text-size: 10px;--mat-button-outlined-horizontal-padding: 15px;--mat-button-text-container-height: 30px;--mat-button-text-label-text-size: 10px;--mat-button-text-horizontal-padding: 15px}.component-box{position:relative;width:100%;flex:1 1 auto;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;background:#fff;border:1px solid #e4e7eb;border-radius:6px;overflow:hidden;transition:box-shadow .12s ease,border-color .12s ease;cursor:default}.component-box.editing .component-title{cursor:grab}.component-box.editing .component-title:active{cursor:grabbing}.component-box.editing .drill-crumb{pointer-events:none}.component-box.editing:hover{border-color:#9db3c8;box-shadow:0 2px 8px #0f172a14}.component-box.selected{border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color),0 4px 14px #2196f32e}.component-box.text{background:transparent;border-color:transparent;border-radius:0;overflow:visible}.component-box.text.editing{border-style:dashed;border-color:#cbd2d9}.component-box.text.editing:hover{box-shadow:none}.component-box.text.selected{border-style:solid;border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color)}:host ::ng-deep .component-filter .filter-bar-container{align-items:center!important}.component-filter{flex:0 0 auto;display:block;padding:0 0 4px}.component-filter .component-title{display:flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;padding-right:40px;font-size:var(--report-heading-size);font-weight:600;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.component-filter .component-title .truncated-icon{font-size:16px;width:16px;height:16px;color:#f59e0b}.component-filter .component-title .drill-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.component-filter .component-title .drill-crumb{color:inherit;cursor:pointer;text-decoration:none}.component-filter .component-title .drill-crumb:hover,.component-filter .component-title .drill-crumb:focus-visible{text-decoration:underline}.component-filter .component-title .drill-separator{opacity:.6}.component-menu{position:absolute;top:2px;right:2px;z-index:30}.component-body{flex:1 1 auto;min-height:0;position:relative}.component-box.refreshing .component-body,.component-box.refreshing .component-menu{opacity:.35;transition:opacity .15s ease}.load-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));z-index:25;pointer-events:none}:host(.height-auto) .component-state{min-height:120px}.drag-veil{position:absolute;inset:0;cursor:grab;touch-action:none}.drag-veil:active{cursor:grabbing}.component-state{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:12px;color:#7b8794;padding:8px;text-align:center}.component-state.error{color:#e15759}.handle{position:absolute;width:14px;height:14px;display:flex;align-items:center;justify-content:center;touch-action:none;z-index:20;transform:scale(calc(1 / var(--canvas-zoom, 1)))}.handle:before{content:\"\";width:8px;height:8px;background:#fff;border:1.5px solid var(--brand-primary-color);border-radius:2px;box-shadow:0 1px 2px #0f172a33}.handle-nw{left:-7px;top:-7px;cursor:nwse-resize}.handle-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}.handle-ne{right:-7px;top:-7px;cursor:nesw-resize}.handle-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}.handle-se{right:-7px;bottom:-7px;cursor:nwse-resize}.handle-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}.handle-sw{left:-7px;bottom:-7px;cursor:nesw-resize}.handle-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}\n"], dependencies: [{ kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { 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: FsFilterModule }, { kind: "component", type: i1$3.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "directive", type: i1$3.FilterHeadingDirective, selector: "[fsFilterHeading]" }, { 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: ComponentChartComponent, selector: "app-report-component-chart", inputs: ["component", "data", "legendSelected", "actions"], outputs: ["legendSelectedChange", "markClick", "markMiss"] }, { kind: "component", type: ComponentGridComponent, selector: "app-report-component-grid", inputs: ["reportId", "component"] }, { kind: "component", type: ComponentKpiComponent, selector: "app-report-component-kpi", inputs: ["component", "data"], outputs: ["markClick"] }, { kind: "component", type: ComponentListComponent, selector: "app-report-component-list", inputs: ["reportId", "component"] }, { kind: "component", type: ComponentTextComponent, selector: "app-report-component-text", inputs: ["html", "tokens", "editing", "editMode", "shownElsewhere"], outputs: ["saved", "editEnded", "valueSettings"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12256
12264
|
}
|
|
12257
12265
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponentComponent, decorators: [{
|
|
12258
12266
|
type: Component,
|
|
@@ -12268,7 +12276,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
12268
12276
|
ComponentKpiComponent,
|
|
12269
12277
|
ComponentListComponent,
|
|
12270
12278
|
ComponentTextComponent,
|
|
12271
|
-
], template: "<div\n class=\"component-box\"\n [class.editing]=\"editMode\"\n [class.text]=\"isText\"\n [class.selected]=\"selected\"\n [class.refreshing]=\"refreshing()\"\n [style.padding]=\"bodyPadding\"\n (pointerdown)=\"onComponentPointerDown($event)\"\n (mousedown)=\"stopCanvasPan($event)\"\n (touchstart)=\"stopCanvasPan($event)\">\n <!-- The header is an fs-filter: heading template = title (and the edit-mode\n drag handle), actions = the menu, items = component-level filter groups,\n the same strip for every type (lists included). fs-filter reads its\n config once, so the block is keyed on the component-filter set to be\n recreated when it changes. The title shows its tokens filled, and a\n filter change repaints it in place (shownTitle). -->\n <!-- A text block has no title bar \u2014 it IS its words, like a text box on a\n slide \u2014 so it gets neither the header nor the data actions. -->\n @for (key of isText ? [] : [filterKey]; track key) {\n <fs-filter\n class=\"component-filter\"\n [config]=\"filterConfig()\">\n <ng-template fsFilterHeading>\n <div\n class=\"component-title\"\n (pointerdown)=\"onDragStart($event)\">\n <!-- While the chart is drilled the title leads its breadcrumb:\n 'Title \u203A 2026 \u203A Q3', the title as the viewer reads it\n (shownTitle). The title goes back to the top and each\n crumb to the period it names; the last is where the chart is.\n A long path is cut at its left end, so the crumbs nearest the\n chart stay in view. While the layout is edited the crumbs are\n plain text (the stylesheet; drillTo does nothing then). -->\n @if (drillCrumbs().length) {\n <span class=\"drill-path\">\n <span dir=\"ltr\">\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo(0)\"\n (keydown.enter)=\"drillTo(0)\">\n {{ shownTitle }}\n </a>\n @for (label of drillCrumbs(); track $index; let last = $last) {\n <span class=\"drill-separator\">{{ drillSeparator }}</span>\n @if (last) {\n <span>{{ label }}</span>\n } @else {\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo($index + 1)\"\n (keydown.enter)=\"drillTo($index + 1)\">\n {{ label }}\n </a>\n }\n }\n </span>\n </span>\n } @else {\n {{ shownTitle }}\n }\n @if (truncated) {\n <mat-icon\n class=\"truncated-icon\"\n matTooltip=\"Showing a truncated result \u2014 refine the filters to see everything.\">\n warning_amber\n </mat-icon>\n }\n </div>\n </ng-template>\n </fs-filter>\n }\n <!-- The actions menu is floated top-right (absolute) rather than living in\n fs-filter's actions slot, so its ~40px button height no longer drives the\n header row's height. It overlays the box corner; the heading reserves\n right padding so a long title ellipsises before it. -->\n @if (!isText || (editMode && !textEditing())) {\n <button\n mat-icon-button\n class=\"component-menu\"\n [fsMenuTriggerFor]=\"menu\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"stopCanvasPan($event)\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <!-- Beside its trigger, inside the same block: an fs-menu that finds no\n trigger of its own draws one. -->\n <fs-menu #menu>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"editText()\">\n <mat-icon>edit</mat-icon>\n <span>Edit text</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"deleteText()\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillUp()\">\n <mat-icon>arrow_upward</mat-icon>\n <span>Drill up</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillTo(0)\">\n <mat-icon>vertical_align_top</mat-icon>\n <span>Back to top</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"settings()\">\n <mat-icon>tune</mat-icon>\n <span>Settings</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportCsv()\">\n <mat-icon>download</mat-icon>\n <span>Export CSV</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportExcel()\">\n <mat-icon>table_view</mat-icon>\n <span>Export Excel</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!hidesColumns\"\n (click)=\"chooseColumns()\">\n <mat-icon>view_column</mat-icon>\n <span>Columns</span>\n </ng-template>\n </fs-menu>\n }\n <div class=\"component-body\">\n @if (isText) {\n <app-report-component-text\n [html]=\"textHtml() ?? component.config?.html ?? ''\"\n [tokens]=\"tokens()\"\n [editing]=\"textEditing()\"\n [shownElsewhere]=\"shownElsewhere\"\n (saved)=\"onTextSaved($event)\"\n (editEnded)=\"textEditing.set(false)\"\n (valueSettings)=\"valueSettings.emit($event)\">\n </app-report-component-text>\n } @else if (component.type === 'list') {\n @for (key of [listResetKey()]; track key) {\n <app-report-component-list\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-list>\n }\n } @else if (component.type === 'grid') {\n <app-report-component-grid\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-grid>\n } @else if (loading()) {\n <!-- Holds the tile's height for the spinner on the first load. -->\n <div class=\"component-state\"></div>\n } @else if (error()) {\n <div class=\"component-state error\">\n {{ error() }}\n </div>\n } @else if (!data()?.rows?.length) {\n <div class=\"component-state\">\n {{ emptyMessage() }}\n </div>\n } @else if (component.type === 'kpi') {\n <app-report-component-kpi\n [component]=\"component\"\n [data]=\"data()\"\n (markClick)=\"onMarkClick($event)\">\n </app-report-component-kpi>\n } @else {\n <app-report-component-chart\n [component]=\"component\"\n [data]=\"data()\"\n [actions]=\"tileActions()\"\n [(legendSelected)]=\"legendSelected\"\n (markClick)=\"onMarkClick($event)\"\n (markMiss)=\"onMarkMiss()\">\n </app-report-component-chart>\n }\n <!-- In edit mode a transparent veil over the body makes the WHOLE\n component draggable (industry standard: you grab the object, not just\n its title bar) and keeps inner widgets from swallowing the gesture. -->\n @if (editMode && !textEditing()) {\n <div\n class=\"drag-veil\"\n (pointerdown)=\"onDragStart($event)\">\n </div>\n }\n </div>\n <!-- A load: a spinner centred on the whole tile. On a refetch the body and\n the menu are dimmed under it (.refreshing); the title is not. -->\n @if (loading() || refreshing()) {\n <mat-progress-spinner\n class=\"load-spinner\"\n aria-label=\"Loading data\"\n mode=\"indeterminate\"\n [diameter]=\"15\">\n </mat-progress-spinner>\n }\n</div>\n<!-- The mark menu, for a click whose mark offers two or more actions. It\n opens at the click: its trigger is a zero-size anchor that\n anchor-at-point.ts moves there first. -->\n@if (!isText && !editMode) {\n <span\n #markAnchor\n [style.position]=\"'absolute'\"\n [fsMenuTriggerFor]=\"markMenu\">\n </span>\n <fs-menu #markMenu>\n @for (action of markMenuActions(); track action) {\n <ng-template\n fs-menu-item\n (click)=\"runMarkAction(action)\">\n {{ markActionLabels[action] }}\n </ng-template>\n }\n </fs-menu>\n}\n<!-- Handles live OUTSIDE the clipped box so they straddle its edges. -->\n@if (editMode && selected) {\n @for (handle of resizeHandles; track handle) {\n <div\n class=\"handle handle-{{ handle }}\"\n (mousedown)=\"stopCanvasPan($event)\"\n (pointerdown)=\"onResizeStart($event, handle)\">\n </div>\n }\n}", styles: [":host{position:absolute;display:flex;flex-direction:column;font-size:10px;--mdc-outlined-button-container-height: 30px;--mdc-outlined-button-label-text-size: 10px;--mat-outlined-button-horizontal-padding: 15px;--mdc-text-button-container-height: 30px;--mdc-text-button-label-text-size: 10px;--mat-text-button-horizontal-padding: 15px;--mat-button-outlined-container-height: 30px;--mat-button-outlined-label-text-size: 10px;--mat-button-outlined-horizontal-padding: 15px;--mat-button-text-container-height: 30px;--mat-button-text-label-text-size: 10px;--mat-button-text-horizontal-padding: 15px}.component-box{position:relative;width:100%;flex:1 1 auto;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;background:#fff;border:1px solid #e4e7eb;border-radius:6px;overflow:hidden;transition:box-shadow .12s ease,border-color .12s ease;cursor:default}.component-box.editing .component-title{cursor:grab}.component-box.editing .component-title:active{cursor:grabbing}.component-box.editing .drill-crumb{pointer-events:none}.component-box.editing:hover{border-color:#9db3c8;box-shadow:0 2px 8px #0f172a14}.component-box.selected{border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color),0 4px 14px #2196f32e}.component-box.text{background:transparent;border-color:transparent;border-radius:0;overflow:visible}.component-box.text.editing{border-style:dashed;border-color:#cbd2d9}.component-box.text.editing:hover{box-shadow:none}.component-box.text.selected{border-style:solid;border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color)}:host ::ng-deep .component-filter .filter-bar-container{align-items:center!important}.component-filter{flex:0 0 auto;display:block;padding:0 0 4px}.component-filter .component-title{display:flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;padding-right:40px;font-size:var(--report-heading-size);font-weight:600;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.component-filter .component-title .truncated-icon{font-size:16px;width:16px;height:16px;color:#f59e0b}.component-filter .component-title .drill-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.component-filter .component-title .drill-crumb{color:inherit;cursor:pointer;text-decoration:none}.component-filter .component-title .drill-crumb:hover,.component-filter .component-title .drill-crumb:focus-visible{text-decoration:underline}.component-filter .component-title .drill-separator{opacity:.6}.component-menu{position:absolute;top:2px;right:2px;z-index:30}.component-body{flex:1 1 auto;min-height:0;position:relative}.component-box.refreshing .component-body,.component-box.refreshing .component-menu{opacity:.35;transition:opacity .15s ease}.load-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));z-index:25;pointer-events:none}:host(.height-auto) .component-state{min-height:120px}.drag-veil{position:absolute;inset:0;cursor:grab;touch-action:none}.drag-veil:active{cursor:grabbing}.component-state{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:12px;color:#7b8794;padding:8px;text-align:center}.component-state.error{color:#e15759}.handle{position:absolute;width:14px;height:14px;display:flex;align-items:center;justify-content:center;touch-action:none;z-index:20;transform:scale(calc(1 / var(--canvas-zoom, 1)))}.handle:before{content:\"\";width:8px;height:8px;background:#fff;border:1.5px solid var(--brand-primary-color);border-radius:2px;box-shadow:0 1px 2px #0f172a33}.handle-nw{left:-7px;top:-7px;cursor:nwse-resize}.handle-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}.handle-ne{right:-7px;top:-7px;cursor:nesw-resize}.handle-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}.handle-se{right:-7px;bottom:-7px;cursor:nwse-resize}.handle-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}.handle-sw{left:-7px;bottom:-7px;cursor:nesw-resize}.handle-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}\n"] }]
|
|
12279
|
+
], template: "<div\n class=\"component-box\"\n [class.editing]=\"editMode\"\n [class.text]=\"isText\"\n [class.selected]=\"selected\"\n [class.refreshing]=\"refreshing()\"\n [style.padding]=\"bodyPadding\"\n (pointerdown)=\"onComponentPointerDown($event)\"\n (mousedown)=\"stopCanvasPan($event)\"\n (touchstart)=\"stopCanvasPan($event)\">\n <!-- The header is an fs-filter: heading template = title (and the edit-mode\n drag handle), actions = the menu, items = component-level filter groups,\n the same strip for every type (lists included). fs-filter reads its\n config once, so the block is keyed on the component-filter set to be\n recreated when it changes. The title shows its tokens filled, and a\n filter change repaints it in place (shownTitle). -->\n <!-- A text block has no title bar \u2014 it IS its words, like a text box on a\n slide \u2014 so it gets neither the header nor the data actions. -->\n @for (key of isText ? [] : [filterKey]; track key) {\n <fs-filter\n class=\"component-filter\"\n [config]=\"filterConfig()\">\n <ng-template fsFilterHeading>\n <div\n class=\"component-title\"\n (pointerdown)=\"onDragStart($event)\">\n <!-- While the chart is drilled the title leads its breadcrumb:\n 'Title \u203A 2026 \u203A Q3', the title as the viewer reads it\n (shownTitle). The title goes back to the top and each\n crumb to the period it names; the last is where the chart is.\n A long path is cut at its left end, so the crumbs nearest the\n chart stay in view. While the layout is edited the crumbs are\n plain text (the stylesheet; drillTo does nothing then). -->\n @if (drillCrumbs().length) {\n <span class=\"drill-path\">\n <span dir=\"ltr\">\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo(0)\"\n (keydown.enter)=\"drillTo(0)\">\n {{ shownTitle }}\n </a>\n @for (label of drillCrumbs(); track $index; let last = $last) {\n <span class=\"drill-separator\">{{ drillSeparator }}</span>\n @if (last) {\n <span>{{ label }}</span>\n } @else {\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo($index + 1)\"\n (keydown.enter)=\"drillTo($index + 1)\">\n {{ label }}\n </a>\n }\n }\n </span>\n </span>\n } @else {\n {{ shownTitle }}\n }\n @if (truncated) {\n <mat-icon\n class=\"truncated-icon\"\n matTooltip=\"Showing a truncated result \u2014 refine the filters to see everything.\">\n warning_amber\n </mat-icon>\n }\n </div>\n </ng-template>\n </fs-filter>\n }\n <!-- The actions menu is floated top-right (absolute) rather than living in\n fs-filter's actions slot, so its ~40px button height no longer drives the\n header row's height. It overlays the box corner; the heading reserves\n right padding so a long title ellipsises before it. -->\n @if (!isText || (editMode && !textEditing())) {\n <button\n mat-icon-button\n class=\"component-menu\"\n [fsMenuTriggerFor]=\"menu\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"stopCanvasPan($event)\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <!-- Beside its trigger, inside the same block: an fs-menu that finds no\n trigger of its own draws one. -->\n <fs-menu #menu>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"editText()\">\n <mat-icon>edit</mat-icon>\n <span>Edit text</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"deleteText()\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillUp()\">\n <mat-icon>arrow_upward</mat-icon>\n <span>Drill up</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillTo(0)\">\n <mat-icon>vertical_align_top</mat-icon>\n <span>Back to top</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"settings()\">\n <mat-icon>tune</mat-icon>\n <span>Settings</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportCsv()\">\n <mat-icon>download</mat-icon>\n <span>Export CSV</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportExcel()\">\n <mat-icon>table_view</mat-icon>\n <span>Export Excel</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!hidesColumns\"\n (click)=\"chooseColumns()\">\n <mat-icon>view_column</mat-icon>\n <span>Columns</span>\n </ng-template>\n </fs-menu>\n }\n <div class=\"component-body\">\n @if (isText) {\n <app-report-component-text\n [html]=\"textHtml() ?? component.config?.html ?? ''\"\n [tokens]=\"tokens()\"\n [editing]=\"textEditing()\"\n [editMode]=\"editMode\"\n [shownElsewhere]=\"shownElsewhere\"\n (saved)=\"onTextSaved($event)\"\n (editEnded)=\"textEditing.set(false)\"\n (valueSettings)=\"valueSettings.emit($event)\">\n </app-report-component-text>\n } @else if (component.type === 'list') {\n @for (key of [listResetKey()]; track key) {\n <app-report-component-list\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-list>\n }\n } @else if (component.type === 'grid') {\n <app-report-component-grid\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-grid>\n } @else if (loading()) {\n <!-- Holds the tile's height for the spinner on the first load. -->\n <div class=\"component-state\"></div>\n } @else if (error()) {\n <div class=\"component-state error\">\n {{ error() }}\n </div>\n } @else if (!data()?.rows?.length) {\n <div class=\"component-state\">\n {{ emptyMessage() }}\n </div>\n } @else if (component.type === 'kpi') {\n <app-report-component-kpi\n [component]=\"component\"\n [data]=\"data()\"\n (markClick)=\"onMarkClick($event)\">\n </app-report-component-kpi>\n } @else {\n <app-report-component-chart\n [component]=\"component\"\n [data]=\"data()\"\n [actions]=\"tileActions()\"\n [(legendSelected)]=\"legendSelected\"\n (markClick)=\"onMarkClick($event)\"\n (markMiss)=\"onMarkMiss()\">\n </app-report-component-chart>\n }\n <!-- In edit mode a transparent veil over the body makes the WHOLE\n component draggable (industry standard: you grab the object, not just\n its title bar) and keeps inner widgets from swallowing the gesture. -->\n @if (editMode && !textEditing()) {\n <div\n class=\"drag-veil\"\n (pointerdown)=\"onDragStart($event)\">\n </div>\n }\n </div>\n <!-- A load: a spinner centred on the whole tile. On a refetch the body and\n the menu are dimmed under it (.refreshing); the title is not. -->\n @if (loading() || refreshing()) {\n <mat-progress-spinner\n class=\"load-spinner\"\n aria-label=\"Loading data\"\n mode=\"indeterminate\"\n [diameter]=\"15\">\n </mat-progress-spinner>\n }\n</div>\n<!-- The mark menu, for a click whose mark offers two or more actions. It\n opens at the click: its trigger is a zero-size anchor that\n anchor-at-point.ts moves there first. -->\n@if (!isText && !editMode) {\n <span\n #markAnchor\n [style.position]=\"'absolute'\"\n [fsMenuTriggerFor]=\"markMenu\">\n </span>\n <fs-menu #markMenu>\n @for (action of markMenuActions(); track action) {\n <ng-template\n fs-menu-item\n (click)=\"runMarkAction(action)\">\n {{ markActionLabels[action] }}\n </ng-template>\n }\n </fs-menu>\n}\n<!-- Handles live OUTSIDE the clipped box so they straddle its edges. -->\n@if (editMode && selected) {\n @for (handle of resizeHandles; track handle) {\n <div\n class=\"handle handle-{{ handle }}\"\n (mousedown)=\"stopCanvasPan($event)\"\n (pointerdown)=\"onResizeStart($event, handle)\">\n </div>\n }\n}", styles: [":host{position:absolute;display:flex;flex-direction:column;font-size:10px;--mdc-outlined-button-container-height: 30px;--mdc-outlined-button-label-text-size: 10px;--mat-outlined-button-horizontal-padding: 15px;--mdc-text-button-container-height: 30px;--mdc-text-button-label-text-size: 10px;--mat-text-button-horizontal-padding: 15px;--mat-button-outlined-container-height: 30px;--mat-button-outlined-label-text-size: 10px;--mat-button-outlined-horizontal-padding: 15px;--mat-button-text-container-height: 30px;--mat-button-text-label-text-size: 10px;--mat-button-text-horizontal-padding: 15px}.component-box{position:relative;width:100%;flex:1 1 auto;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;background:#fff;border:1px solid #e4e7eb;border-radius:6px;overflow:hidden;transition:box-shadow .12s ease,border-color .12s ease;cursor:default}.component-box.editing .component-title{cursor:grab}.component-box.editing .component-title:active{cursor:grabbing}.component-box.editing .drill-crumb{pointer-events:none}.component-box.editing:hover{border-color:#9db3c8;box-shadow:0 2px 8px #0f172a14}.component-box.selected{border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color),0 4px 14px #2196f32e}.component-box.text{background:transparent;border-color:transparent;border-radius:0;overflow:visible}.component-box.text.editing{border-style:dashed;border-color:#cbd2d9}.component-box.text.editing:hover{box-shadow:none}.component-box.text.selected{border-style:solid;border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color)}:host ::ng-deep .component-filter .filter-bar-container{align-items:center!important}.component-filter{flex:0 0 auto;display:block;padding:0 0 4px}.component-filter .component-title{display:flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;padding-right:40px;font-size:var(--report-heading-size);font-weight:600;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.component-filter .component-title .truncated-icon{font-size:16px;width:16px;height:16px;color:#f59e0b}.component-filter .component-title .drill-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.component-filter .component-title .drill-crumb{color:inherit;cursor:pointer;text-decoration:none}.component-filter .component-title .drill-crumb:hover,.component-filter .component-title .drill-crumb:focus-visible{text-decoration:underline}.component-filter .component-title .drill-separator{opacity:.6}.component-menu{position:absolute;top:2px;right:2px;z-index:30}.component-body{flex:1 1 auto;min-height:0;position:relative}.component-box.refreshing .component-body,.component-box.refreshing .component-menu{opacity:.35;transition:opacity .15s ease}.load-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));z-index:25;pointer-events:none}:host(.height-auto) .component-state{min-height:120px}.drag-veil{position:absolute;inset:0;cursor:grab;touch-action:none}.drag-veil:active{cursor:grabbing}.component-state{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:12px;color:#7b8794;padding:8px;text-align:center}.component-state.error{color:#e15759}.handle{position:absolute;width:14px;height:14px;display:flex;align-items:center;justify-content:center;touch-action:none;z-index:20;transform:scale(calc(1 / var(--canvas-zoom, 1)))}.handle:before{content:\"\";width:8px;height:8px;background:#fff;border:1.5px solid var(--brand-primary-color);border-radius:2px;box-shadow:0 1px 2px #0f172a33}.handle-nw{left:-7px;top:-7px;cursor:nwse-resize}.handle-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}.handle-ne{right:-7px;top:-7px;cursor:nesw-resize}.handle-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}.handle-se{right:-7px;bottom:-7px;cursor:nwse-resize}.handle-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}.handle-sw{left:-7px;bottom:-7px;cursor:nesw-resize}.handle-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}\n"] }]
|
|
12272
12280
|
}], propDecorators: { reportId: [{
|
|
12273
12281
|
type: Input
|
|
12274
12282
|
}], component: [{
|
|
@@ -15411,7 +15419,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
15411
15419
|
// in the report's format context, as on screen.
|
|
15412
15420
|
async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
|
|
15413
15421
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
15414
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
15422
|
+
const echarts = (await import('./firestitch-report-echarts-z76sZHp7.mjs')).default;
|
|
15415
15423
|
const host = document.createElement('div');
|
|
15416
15424
|
host.style.width = `${width}px`;
|
|
15417
15425
|
host.style.height = `${height}px`;
|
|
@@ -15722,6 +15730,18 @@ const NO_BAND_MARGIN = 0.5 * PT;
|
|
|
15722
15730
|
const NO_BANDS = { headerHeight: 0, footerHeight: 0 };
|
|
15723
15731
|
// The last row of a list box whose rows carry on past it.
|
|
15724
15732
|
const CONTINUED_NOTICE = 'Continued on the following pages';
|
|
15733
|
+
// The last row of a list box whose rows have nowhere to go: its heading and
|
|
15734
|
+
// totals row, wrapped tall in narrow columns, leave a continuation page no
|
|
15735
|
+
// room for a row.
|
|
15736
|
+
const NO_ROOM_NOTICE = 'Not enough room for this table — enlarge the tile';
|
|
15737
|
+
// A list's or a table tile's table: its font size and cell padding, points.
|
|
15738
|
+
const TABLE_FONT_SIZE = 8;
|
|
15739
|
+
const TABLE_CELL_PADDING = 3;
|
|
15740
|
+
// What a component's box says when it could not be drawn.
|
|
15741
|
+
const UNDRAWN_NOTE = 'Could not be drawn';
|
|
15742
|
+
// Thrown from autoTable's page hook to stop an on-page table at the first page
|
|
15743
|
+
// it would add (_drawTable).
|
|
15744
|
+
const STRAY_PAGE = new Error('The table ran past its page');
|
|
15725
15745
|
// A text line's height as a multiple of its largest font size — the canvas
|
|
15726
15746
|
// stylesheet's line-height, so a block wraps and stacks the same way.
|
|
15727
15747
|
const TEXT_LINE_HEIGHT = 1.25;
|
|
@@ -15744,7 +15764,14 @@ const GRID_INDENT$1 = 8;
|
|
|
15744
15764
|
// row-height estimate, so it cannot spill past the box. The rows that do not
|
|
15745
15765
|
// fit continue on pages of their own after the report page, under the same
|
|
15746
15766
|
// header/footer and with the same column widths, up to PDF_LIST_ROWS; a
|
|
15747
|
-
// longer result ends in a row pointing to the component's CSV export.
|
|
15767
|
+
// longer result ends in a row pointing to the component's CSV export. A
|
|
15768
|
+
// table whose heading and totals row leave a continuation page no room for a
|
|
15769
|
+
// row stops at its box with a notice instead: autoTable would add pages
|
|
15770
|
+
// without end trying to place it.
|
|
15771
|
+
//
|
|
15772
|
+
// A component that fails to draw is marked in its box and the file goes on
|
|
15773
|
+
// (_guarded); build() hands back the ones that did, as it does the text it
|
|
15774
|
+
// cut.
|
|
15748
15775
|
//
|
|
15749
15776
|
// A text block's tables are drawn by autoTable too, from the line its words
|
|
15750
15777
|
// reach, in the column widths the canvas gives them (ReportTextLayoutService).
|
|
@@ -15766,12 +15793,16 @@ class ReportPdfService {
|
|
|
15766
15793
|
// Each continuation appends its pages at the end of the document, so
|
|
15767
15794
|
// two lists that overflowed on one page follow it back to back.
|
|
15768
15795
|
for (const overflow of overflows) {
|
|
15769
|
-
|
|
15796
|
+
const tokens = this._tokens(context, collectedPage.page, index, pageNumber + 1);
|
|
15797
|
+
await this._guarded(context, overflow.component, tokens.pageName, tokens, false, async () => {
|
|
15798
|
+
pageNumber = await this._renderContinuation(context, collectedPage.page, index, pageNumber, overflow);
|
|
15799
|
+
});
|
|
15770
15800
|
}
|
|
15771
15801
|
}
|
|
15772
15802
|
return {
|
|
15773
15803
|
overflows: [...context.textOverflows.values()],
|
|
15774
15804
|
overflowNote: PDF_OVERFLOW_NOTE,
|
|
15805
|
+
undrawn: [...context.undrawn.values()],
|
|
15775
15806
|
save: () => {
|
|
15776
15807
|
context.doc.save(`${this._slug(collected.report.name)}.pdf`);
|
|
15777
15808
|
},
|
|
@@ -15780,13 +15811,14 @@ class ReportPdfService {
|
|
|
15780
15811
|
async _context(collected) {
|
|
15781
15812
|
const report = collected.report;
|
|
15782
15813
|
const { jsPDF } = await import('jspdf');
|
|
15783
|
-
const autoTable =
|
|
15814
|
+
const { default: autoTable, __createTable: createTable } = await import('jspdf-autotable');
|
|
15784
15815
|
const pageW = report.pageWidth * PT;
|
|
15785
15816
|
const pageH = report.pageHeight * PT;
|
|
15786
15817
|
const orientation = pageW >= pageH ? 'landscape' : 'portrait';
|
|
15787
15818
|
return {
|
|
15788
15819
|
doc: new jsPDF({ orientation, unit: 'pt', format: [pageW, pageH] }),
|
|
15789
15820
|
autoTable,
|
|
15821
|
+
createTable,
|
|
15790
15822
|
jsPDF,
|
|
15791
15823
|
report,
|
|
15792
15824
|
timezone: resolveReportTimezone(report),
|
|
@@ -15798,6 +15830,7 @@ class ReportPdfService {
|
|
|
15798
15830
|
filterFields: collected.filterFields,
|
|
15799
15831
|
masterFilters: layerFilterFields(collected.filterFields, report.filterGroups ?? [], null),
|
|
15800
15832
|
textOverflows: new Map(),
|
|
15833
|
+
undrawn: new Map(),
|
|
15801
15834
|
};
|
|
15802
15835
|
}
|
|
15803
15836
|
// One report page: the header and footer first, under the page's own
|
|
@@ -15810,14 +15843,59 @@ class ReportPdfService {
|
|
|
15810
15843
|
doc.addPage([context.pageW, context.pageH], context.orientation);
|
|
15811
15844
|
await this._renderMaster(context, collectedPage.page, tokens);
|
|
15812
15845
|
for (const item of collectedPage.components) {
|
|
15813
|
-
|
|
15814
|
-
|
|
15846
|
+
await this._guarded(context, item.component, tokens.pageName, pageTokens, true, async () => {
|
|
15847
|
+
if (item.component.type !== 'text') {
|
|
15848
|
+
await this._renderComponent(context, collectedPage.page, item, tokens, overflows);
|
|
15849
|
+
}
|
|
15850
|
+
else if (await this._renderText(context, item.component, pageTokens)) {
|
|
15851
|
+
this._recordOverflow(context, item.component, tokens.pageName, pageTokens);
|
|
15852
|
+
}
|
|
15853
|
+
});
|
|
15854
|
+
}
|
|
15855
|
+
}
|
|
15856
|
+
// Draws one component, or one list's continuation pages, so that whatever
|
|
15857
|
+
// goes wrong stays with it: a draw that throws loses the pages it added, its
|
|
15858
|
+
// box on the page (boxed) is blanked under UNDRAWN_NOTE, and the export's
|
|
15859
|
+
// gate names it under page. One component can then never stop the file.
|
|
15860
|
+
async _guarded(context, component, page, tokens, boxed, draw) {
|
|
15861
|
+
const { doc } = context;
|
|
15862
|
+
const current = doc.getCurrentPageInfo().pageNumber;
|
|
15863
|
+
const pages = doc.getNumberOfPages();
|
|
15864
|
+
try {
|
|
15865
|
+
await draw();
|
|
15866
|
+
}
|
|
15867
|
+
catch (error) {
|
|
15868
|
+
// The file only says the box could not be drawn; the console keeps why.
|
|
15869
|
+
console.error(error);
|
|
15870
|
+
while (doc.getNumberOfPages() > pages) {
|
|
15871
|
+
doc.deletePage(doc.getNumberOfPages());
|
|
15815
15872
|
}
|
|
15816
|
-
|
|
15817
|
-
|
|
15873
|
+
doc.setPage(current);
|
|
15874
|
+
if (boxed) {
|
|
15875
|
+
this._undrawnBox(doc, component);
|
|
15876
|
+
}
|
|
15877
|
+
if (!context.undrawn.has(component.id)) {
|
|
15878
|
+
context.undrawn.set(component.id, {
|
|
15879
|
+
componentId: component.id,
|
|
15880
|
+
page,
|
|
15881
|
+
name: component.type === 'text'
|
|
15882
|
+
? textOverflow(component, page, tokens).snippet
|
|
15883
|
+
: exportTitle(component, tokens),
|
|
15884
|
+
});
|
|
15818
15885
|
}
|
|
15819
15886
|
}
|
|
15820
15887
|
}
|
|
15888
|
+
// A component's box blanked under UNDRAWN_NOTE, over whatever of it was
|
|
15889
|
+
// drawn before it failed.
|
|
15890
|
+
_undrawnBox(doc, component) {
|
|
15891
|
+
const x = component.x * PT;
|
|
15892
|
+
const y = component.y * PT;
|
|
15893
|
+
const w = component.w * PT;
|
|
15894
|
+
const h = component.h * PT;
|
|
15895
|
+
doc.setFillColor('#ffffff');
|
|
15896
|
+
doc.rect(x, y, w, h, 'F');
|
|
15897
|
+
this._boxNote(doc, UNDRAWN_NOTE, x, y, w, h);
|
|
15898
|
+
}
|
|
15821
15899
|
// The header and footer, their value tokens filled with the master's values
|
|
15822
15900
|
// whichever page they sit on: a report page or a continuation page. They
|
|
15823
15901
|
// read no page filter: its token prints the em dash there.
|
|
@@ -15827,9 +15905,11 @@ class ReportPdfService {
|
|
|
15827
15905
|
}
|
|
15828
15906
|
const masterTokens = { ...tokens, values: context.masterValues, filters: context.masterFilters };
|
|
15829
15907
|
for (const component of (context.report.master?.components ?? []).filter(isDrawn)) {
|
|
15830
|
-
|
|
15831
|
-
this.
|
|
15832
|
-
|
|
15908
|
+
await this._guarded(context, component, HEADER_FOOTER_NAME, masterTokens, true, async () => {
|
|
15909
|
+
if (await this._renderText(context, component, masterTokens)) {
|
|
15910
|
+
this._recordOverflow(context, component, HEADER_FOOTER_NAME, masterTokens);
|
|
15911
|
+
}
|
|
15912
|
+
});
|
|
15833
15913
|
}
|
|
15834
15914
|
}
|
|
15835
15915
|
// Lists a text block that was cut, under the page the export's overflow gate
|
|
@@ -15955,7 +16035,8 @@ class ReportPdfService {
|
|
|
15955
16035
|
// wraps them, and the words are drawn into each cell once autoTable has
|
|
15956
16036
|
// drawn its grid lines and fill (_drawTextCell), so they keep their runs'
|
|
15957
16037
|
// fonts and colors. Only the rows the box holds are drawn, and a table that
|
|
15958
|
-
// loses any is cut. The cursor it leaves is
|
|
16038
|
+
// loses any, or runs past its page anyway, is cut. The cursor it leaves is
|
|
16039
|
+
// TEXT_TABLE_MARGIN under it.
|
|
15959
16040
|
_drawTextTable(context, table, layout, box, top) {
|
|
15960
16041
|
if (top >= box.bottom) {
|
|
15961
16042
|
return { cursorY: top, cut: true };
|
|
@@ -15967,13 +16048,11 @@ class ReportPdfService {
|
|
|
15967
16048
|
: Array.from({ length: table.columns }, () => width / table.columns);
|
|
15968
16049
|
const body = table.rows.map((row) => row.cells.map((cell) => this._textCellInput(context.doc, cell, columnWidths)));
|
|
15969
16050
|
const options = this._textTableOptions(context, columnWidths, left, top, width, box.bottom);
|
|
15970
|
-
const fit = this._textTableFit(context, { ...options, body });
|
|
15971
|
-
|
|
15972
|
-
this._drawTable(context, { ...options, body: this._keptRows(body, fit) });
|
|
15973
|
-
}
|
|
16051
|
+
const fit = this._textTableFit(context, { ...options, body }, top, box.bottom);
|
|
16052
|
+
const whole = !fit || this._drawTable(context, { ...options, body: this._keptRows(body, fit) });
|
|
15974
16053
|
return {
|
|
15975
|
-
cursorY: fit ? context.doc.lastAutoTable.finalY + TEXT_TABLE_MARGIN : top,
|
|
15976
|
-
cut: fit < table.rows.length,
|
|
16054
|
+
cursorY: fit && whole ? context.doc.lastAutoTable.finalY + TEXT_TABLE_MARGIN : top,
|
|
16055
|
+
cut: !whole || fit < table.rows.length,
|
|
15977
16056
|
};
|
|
15978
16057
|
}
|
|
15979
16058
|
// A text table's cell as autoTable takes it: its spans and fill and, when it
|
|
@@ -16027,22 +16106,10 @@ class ReportPdfService {
|
|
|
16027
16106
|
didDrawCell: (hook) => this._drawTextCell(hook),
|
|
16028
16107
|
};
|
|
16029
16108
|
}
|
|
16030
|
-
// How many of a text table's rows its box
|
|
16031
|
-
//
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
_textTableFit(context, options) {
|
|
16035
|
-
const scratch = this._scratchDocument(context);
|
|
16036
|
-
let fit = 0;
|
|
16037
|
-
context.autoTable(scratch, {
|
|
16038
|
-
...options,
|
|
16039
|
-
didDrawCell: (hook) => {
|
|
16040
|
-
if (scratch.getCurrentPageInfo().pageNumber === 1 && !hook.row.spansMultiplePages) {
|
|
16041
|
-
fit = Math.max(fit, hook.row.index + 1);
|
|
16042
|
-
}
|
|
16043
|
-
},
|
|
16044
|
-
});
|
|
16045
|
-
return fit;
|
|
16109
|
+
// How many of a text table's rows its box, from top to bottom, holds whole:
|
|
16110
|
+
// the table laid out as _measure lays out a list's.
|
|
16111
|
+
_textTableFit(context, options, top, bottom) {
|
|
16112
|
+
return this._fittingRows(context.createTable(this._scratchDocument(context), options), top, bottom, 0);
|
|
16046
16113
|
}
|
|
16047
16114
|
// The first `fit` rows, a cell that spans down past the last of them cut
|
|
16048
16115
|
// there.
|
|
@@ -16166,7 +16233,7 @@ class ReportPdfService {
|
|
|
16166
16233
|
};
|
|
16167
16234
|
}
|
|
16168
16235
|
// A tile on its report page, its title's tokens filled with that page's.
|
|
16169
|
-
async _renderComponent(context, item, tokens, overflows) {
|
|
16236
|
+
async _renderComponent(context, page, item, tokens, overflows) {
|
|
16170
16237
|
const { doc } = context;
|
|
16171
16238
|
const component = item.component;
|
|
16172
16239
|
// Geometry is stored in inches; the PDF draws in points.
|
|
@@ -16207,7 +16274,7 @@ class ReportPdfService {
|
|
|
16207
16274
|
}
|
|
16208
16275
|
case 'list':
|
|
16209
16276
|
case 'grid':
|
|
16210
|
-
this._renderTable(context, item, x, bodyY, w, bodyH, overflows);
|
|
16277
|
+
this._renderTable(context, page, item, x, bodyY, w, bodyH, overflows);
|
|
16211
16278
|
break;
|
|
16212
16279
|
case 'kpi':
|
|
16213
16280
|
this._renderKpi(context, item, x, bodyY, w, bodyH);
|
|
@@ -16245,42 +16312,51 @@ class ReportPdfService {
|
|
|
16245
16312
|
doc.text(this._pdfText(title), x, y + 10, { maxWidth: w });
|
|
16246
16313
|
}
|
|
16247
16314
|
// An italic grey line centered in a component's body, for a body that holds
|
|
16248
|
-
// something other than its content.
|
|
16315
|
+
// something other than its content. A line wider than the body wraps.
|
|
16249
16316
|
_boxNote(doc, text, x, y, w, h) {
|
|
16250
16317
|
doc.setFont('helvetica', 'italic');
|
|
16251
16318
|
doc.setFontSize(9);
|
|
16252
16319
|
doc.setTextColor(MUTED_COLOR$1);
|
|
16253
|
-
doc.text(text, x + w / 2, y + h / 2, { align: 'center' });
|
|
16320
|
+
doc.text(text, x + w / 2, y + h / 2, { align: 'center', maxWidth: w });
|
|
16254
16321
|
}
|
|
16255
16322
|
// A list's on-page table. All of its fetched rows are laid out once to learn
|
|
16256
16323
|
// how many the box holds; when they do not all fit, the box's last row says
|
|
16257
16324
|
// the table continues and the rest go to overflows for continuation pages.
|
|
16258
16325
|
// A box too short for even the head carries the notice as a line instead.
|
|
16259
|
-
// A
|
|
16260
|
-
//
|
|
16261
|
-
|
|
16326
|
+
// A table whose heading and totals row leave a continuation page no room for
|
|
16327
|
+
// a row stops at its box, its notice saying so. A totals row closes the
|
|
16328
|
+
// table once: in the box when every row fits it, else after the last
|
|
16329
|
+
// continuation row. Points throughout.
|
|
16330
|
+
_renderTable(context, page, item, x, y, w, h, overflows) {
|
|
16262
16331
|
const { component, search, filtered } = item;
|
|
16263
16332
|
const { head, cells, numeric, foot, omitted } = item.table ? this._gridCells(item) : this._listCells(context, item);
|
|
16264
16333
|
const bottom = context.pageH - (y + h);
|
|
16265
|
-
const { fit, columnStyles } = this._measure(context, head, cells, numeric, foot, x, y, w, bottom);
|
|
16334
|
+
const { fit, columnStyles, minHeight } = this._measure(context, head, cells, numeric, foot, x, y, w, bottom);
|
|
16266
16335
|
if (fit === cells.length && omitted === 0) {
|
|
16267
16336
|
this._drawTable(context, this._tableOptions(context, head, numeric, cells, x, y, w, bottom, columnStyles, foot));
|
|
16268
16337
|
return;
|
|
16269
16338
|
}
|
|
16339
|
+
// autoTable needs room for a row under the heading on every page it adds,
|
|
16340
|
+
// or it adds pages until the browser's stack runs out.
|
|
16341
|
+
const continues = minHeight < this._continuationRoom(context, page);
|
|
16342
|
+
const notice = continues ? CONTINUED_NOTICE : NO_ROOM_NOTICE;
|
|
16270
16343
|
// The notice takes the place of the last row that fit, so that row moves
|
|
16271
16344
|
// to the continuation. omitted > 0 means a full PDF_LIST_ROWS fetch, which
|
|
16272
16345
|
// never fits one box, so the CSV notice always lands on a continuation
|
|
16273
16346
|
// page.
|
|
16274
16347
|
const kept = Math.max(0, fit - 1);
|
|
16275
|
-
overflows.push({
|
|
16276
|
-
component, search, crumbs: item.crumbs, filtered, head, numeric, rows: cells.slice(kept), omitted, foot, x, w, columnStyles,
|
|
16277
|
-
});
|
|
16278
16348
|
if (fit >= 1) {
|
|
16279
|
-
const body = [...cells.slice(0, kept), this._noticeRow(
|
|
16349
|
+
const body = [...cells.slice(0, kept), this._noticeRow(notice, numeric.length, true)];
|
|
16280
16350
|
this._drawTable(context, this._tableOptions(context, head, numeric, body, x, y, w, bottom, columnStyles));
|
|
16281
16351
|
}
|
|
16282
16352
|
else {
|
|
16283
|
-
this._boxNote(context.doc,
|
|
16353
|
+
this._boxNote(context.doc, notice, x, y, w, h);
|
|
16354
|
+
}
|
|
16355
|
+
if (continues) {
|
|
16356
|
+
overflows.push({
|
|
16357
|
+
component, search, crumbs: item.crumbs, filtered, head, numeric, rows: cells.slice(kept), omitted, foot, x, w,
|
|
16358
|
+
columnStyles,
|
|
16359
|
+
});
|
|
16284
16360
|
}
|
|
16285
16361
|
}
|
|
16286
16362
|
// A list's heading, rows, totals row and the rows past its fetch.
|
|
@@ -16302,7 +16378,16 @@ class ReportPdfService {
|
|
|
16302
16378
|
rowSpan: exported.rowSpan,
|
|
16303
16379
|
styles: {
|
|
16304
16380
|
...(exported.bold ? { fontStyle: 'bold' } : {}),
|
|
16305
|
-
...(exported.indent
|
|
16381
|
+
...(exported.indent
|
|
16382
|
+
? {
|
|
16383
|
+
cellPadding: {
|
|
16384
|
+
top: TABLE_CELL_PADDING,
|
|
16385
|
+
right: TABLE_CELL_PADDING,
|
|
16386
|
+
bottom: TABLE_CELL_PADDING,
|
|
16387
|
+
left: TABLE_CELL_PADDING + exported.indent * GRID_INDENT$1,
|
|
16388
|
+
},
|
|
16389
|
+
}
|
|
16390
|
+
: {}),
|
|
16306
16391
|
},
|
|
16307
16392
|
});
|
|
16308
16393
|
return {
|
|
@@ -16345,7 +16430,13 @@ class ReportPdfService {
|
|
|
16345
16430
|
margin: { left: x, right: context.pageW - x - w, bottom },
|
|
16346
16431
|
tableWidth: w,
|
|
16347
16432
|
rowPageBreak: 'avoid',
|
|
16348
|
-
styles: {
|
|
16433
|
+
styles: {
|
|
16434
|
+
font: 'helvetica',
|
|
16435
|
+
fontStyle: 'normal',
|
|
16436
|
+
fontSize: TABLE_FONT_SIZE,
|
|
16437
|
+
cellPadding: TABLE_CELL_PADDING,
|
|
16438
|
+
textColor: '#1f2933',
|
|
16439
|
+
},
|
|
16349
16440
|
headStyles: { fillColor: '#4e79a7', textColor: '#ffffff', fontStyle: 'bold' },
|
|
16350
16441
|
alternateRowStyles: { fillColor: '#f5f7fa' },
|
|
16351
16442
|
...(columnStyles ? { columnStyles } : {}),
|
|
@@ -16383,37 +16474,51 @@ class ReportPdfService {
|
|
|
16383
16474
|
}
|
|
16384
16475
|
// Lays every fetched row out once on a scratch document of the same page
|
|
16385
16476
|
// size, with the options the real draw uses, and reads back how many body
|
|
16386
|
-
// rows the box
|
|
16387
|
-
//
|
|
16388
|
-
//
|
|
16389
|
-
//
|
|
16390
|
-
//
|
|
16391
|
-
//
|
|
16392
|
-
//
|
|
16477
|
+
// rows the box holds (fit), the column widths autoTable chose, with each
|
|
16478
|
+
// numeric column's right alignment merged in, so the on-page table and its
|
|
16479
|
+
// continuation pages carry both, and the least room a page must give the
|
|
16480
|
+
// table (minHeight). The totals row is laid out too, so the widths hold it,
|
|
16481
|
+
// and a table whose rows fit the box but whose totals row does not moves
|
|
16482
|
+
// its last row on. fit 0 means not even one body row fits under the head.
|
|
16483
|
+
// The table is laid out, never drawn: drawn, autoTable pages it, and a table
|
|
16484
|
+
// with no room for a row on a page pages without end.
|
|
16393
16485
|
_measure(context, head, cells, numeric, foot, x, y, w, bottom) {
|
|
16394
16486
|
const scratch = this._scratchDocument(context);
|
|
16395
|
-
|
|
16396
|
-
|
|
16397
|
-
...this._tableOptions(context, head, numeric, cells, x, y, w, bottom, undefined, foot),
|
|
16398
|
-
didDrawCell: (hook) => {
|
|
16399
|
-
if (hook.section === 'body' && scratch.getCurrentPageInfo().pageNumber === 1) {
|
|
16400
|
-
fit = Math.max(fit, hook.row.index + 1);
|
|
16401
|
-
}
|
|
16402
|
-
},
|
|
16403
|
-
});
|
|
16404
|
-
const columns = scratch.lastAutoTable.columns;
|
|
16487
|
+
const table = context.createTable(scratch, this._tableOptions(context, head, numeric, cells, x, y, w, bottom, undefined, foot));
|
|
16488
|
+
const columns = table.columns;
|
|
16405
16489
|
const columnStyles = {};
|
|
16406
16490
|
columns.forEach((column, index) => {
|
|
16407
16491
|
columnStyles[index] = numeric[index]
|
|
16408
16492
|
? { cellWidth: column.width, halign: 'right' }
|
|
16409
16493
|
: { cellWidth: column.width };
|
|
16410
16494
|
});
|
|
16411
|
-
|
|
16412
|
-
|
|
16413
|
-
|
|
16414
|
-
fit
|
|
16495
|
+
const headHeight = table.getHeadHeight(table.columns);
|
|
16496
|
+
const footHeight = table.getFootHeight(table.columns);
|
|
16497
|
+
return {
|
|
16498
|
+
fit: this._fittingRows(table, y + headHeight, context.pageH - bottom, footHeight),
|
|
16499
|
+
columnStyles,
|
|
16500
|
+
// One line of a row: the line autoTable must place on a page before it
|
|
16501
|
+
// moves on, at the table's font size and padding.
|
|
16502
|
+
minHeight: headHeight + footHeight + TABLE_FONT_SIZE * scratch.getLineHeightFactor() + 2 * TABLE_CELL_PADDING,
|
|
16503
|
+
};
|
|
16504
|
+
}
|
|
16505
|
+
// How many of a laid-out table's body rows fit whole between top and
|
|
16506
|
+
// bottom, as autoTable decides it: a row fits when its tallest cell (one that
|
|
16507
|
+
// spans rows counts all of them) ends by bottom, the last row's by the top
|
|
16508
|
+
// of the totals row (footHeight) under it, and the next row starts where it
|
|
16509
|
+
// ends.
|
|
16510
|
+
_fittingRows(table, top, bottom, footHeight) {
|
|
16511
|
+
let cursor = top;
|
|
16512
|
+
let fit = 0;
|
|
16513
|
+
for (const [index, row] of table.body.entries()) {
|
|
16514
|
+
const limit = index === table.body.length - 1 ? bottom - footHeight : bottom;
|
|
16515
|
+
if (cursor + row.getMaxCellHeight(table.columns) > limit) {
|
|
16516
|
+
break;
|
|
16517
|
+
}
|
|
16518
|
+
cursor += row.height;
|
|
16519
|
+
fit += 1;
|
|
16415
16520
|
}
|
|
16416
|
-
return
|
|
16521
|
+
return fit;
|
|
16417
16522
|
}
|
|
16418
16523
|
// A blank document of the export's page size, for laying a table out to see
|
|
16419
16524
|
// what fits before the real one is drawn on.
|
|
@@ -16424,20 +16529,42 @@ class ReportPdfService {
|
|
|
16424
16529
|
orientation: context.orientation,
|
|
16425
16530
|
});
|
|
16426
16531
|
}
|
|
16427
|
-
// Draws an on-page table (a list's, or a text block's)
|
|
16428
|
-
//
|
|
16429
|
-
//
|
|
16430
|
-
//
|
|
16431
|
-
// remaining components
|
|
16532
|
+
// Draws an on-page table (a list's, or a text block's) on its page alone.
|
|
16533
|
+
// That is all it should need: the measured fit, the identical column widths
|
|
16534
|
+
// and a single-line notice keep the table inside its box. Should autoTable
|
|
16535
|
+
// start another page anyway, the table stops there and the page goes, so a
|
|
16536
|
+
// stray page can never catch the report page's remaining components, nor a
|
|
16537
|
+
// table with no room for a row add pages without end. Returns whether the
|
|
16538
|
+
// table was drawn whole.
|
|
16432
16539
|
_drawTable(context, options) {
|
|
16433
16540
|
const { doc } = context;
|
|
16434
16541
|
const page = doc.getCurrentPageInfo().pageNumber;
|
|
16435
16542
|
const pages = doc.getNumberOfPages();
|
|
16436
|
-
|
|
16543
|
+
let whole = true;
|
|
16544
|
+
try {
|
|
16545
|
+
context.autoTable(doc, {
|
|
16546
|
+
...options,
|
|
16547
|
+
// Fires as the table starts its own page, and again on each page it
|
|
16548
|
+
// moves to (an existing page, such as a continuation page under a
|
|
16549
|
+
// header, as well as a new one).
|
|
16550
|
+
willDrawPage: () => {
|
|
16551
|
+
if (doc.getCurrentPageInfo().pageNumber !== page) {
|
|
16552
|
+
throw STRAY_PAGE;
|
|
16553
|
+
}
|
|
16554
|
+
},
|
|
16555
|
+
});
|
|
16556
|
+
}
|
|
16557
|
+
catch (error) {
|
|
16558
|
+
if (error !== STRAY_PAGE) {
|
|
16559
|
+
throw error;
|
|
16560
|
+
}
|
|
16561
|
+
whole = false;
|
|
16562
|
+
}
|
|
16437
16563
|
while (doc.getNumberOfPages() > pages) {
|
|
16438
16564
|
doc.deletePage(doc.getNumberOfPages());
|
|
16439
16565
|
}
|
|
16440
16566
|
doc.setPage(page);
|
|
16567
|
+
return whole;
|
|
16441
16568
|
}
|
|
16442
16569
|
// The rows of one list that did not fit its box, on pages appended after
|
|
16443
16570
|
// the report page, each with the page's header and footer, the table's
|
|
@@ -16446,9 +16573,7 @@ class ReportPdfService {
|
|
|
16446
16573
|
// Returns the physical page counter after these pages.
|
|
16447
16574
|
async _renderContinuation(context, page, index, pageNumber, overflow) {
|
|
16448
16575
|
const { doc } = context;
|
|
16449
|
-
const
|
|
16450
|
-
const top = this._continuationMargin(bands.headerHeight);
|
|
16451
|
-
const bottom = this._continuationMargin(bands.footerHeight);
|
|
16576
|
+
const { top, bottom } = this._continuationMargins(context, page);
|
|
16452
16577
|
const { x, w } = overflow;
|
|
16453
16578
|
const body = overflow.omitted > 0
|
|
16454
16579
|
? [...overflow.rows, this._noticeRow(moreRowsNotice(overflow.omitted), overflow.numeric.length, false)]
|
|
@@ -16482,6 +16607,21 @@ class ReportPdfService {
|
|
|
16482
16607
|
}
|
|
16483
16608
|
doc.setPage(doc.getNumberOfPages());
|
|
16484
16609
|
}
|
|
16610
|
+
// The margins a continuation table keeps from the top and bottom of its
|
|
16611
|
+
// pages, under its title's strip at the top.
|
|
16612
|
+
_continuationMargins(context, page) {
|
|
16613
|
+
const bands = page.masterHidden ? NO_BANDS : context.report.config?.headerFooter ?? NO_BANDS;
|
|
16614
|
+
return {
|
|
16615
|
+
top: this._continuationMargin(bands.headerHeight),
|
|
16616
|
+
bottom: this._continuationMargin(bands.footerHeight),
|
|
16617
|
+
};
|
|
16618
|
+
}
|
|
16619
|
+
// The height a continuation page gives its table: between its margins, under
|
|
16620
|
+
// the title.
|
|
16621
|
+
_continuationRoom(context, page) {
|
|
16622
|
+
const { top, bottom } = this._continuationMargins(context, page);
|
|
16623
|
+
return context.pageH - top - TITLE_H$1 - bottom;
|
|
16624
|
+
}
|
|
16485
16625
|
// The margin a continuation table keeps from one edge: inside the band when
|
|
16486
16626
|
// the page has one.
|
|
16487
16627
|
_continuationMargin(band) {
|
|
@@ -16847,6 +16987,9 @@ class ReportPptxService {
|
|
|
16847
16987
|
return {
|
|
16848
16988
|
overflows: [...overflows.values()],
|
|
16849
16989
|
overflowNote: PPTX_OVERFLOW_NOTE,
|
|
16990
|
+
// A slide's shapes are laid out by PowerPoint, so a component cannot
|
|
16991
|
+
// fail to draw here as a PDF table can.
|
|
16992
|
+
undrawn: [],
|
|
16850
16993
|
save: async () => {
|
|
16851
16994
|
await pptx.writeFile({ fileName: `${this._slug(report.name)}.pptx` });
|
|
16852
16995
|
},
|
|
@@ -18025,7 +18168,8 @@ const RETRY = 'retry';
|
|
|
18025
18168
|
const DOWNLOAD = 'download';
|
|
18026
18169
|
// Runs a report export end to end: collect every tile and value, refuse to
|
|
18027
18170
|
// write a file that is missing any of them without saying so, build it, refuse
|
|
18028
|
-
// to save one whose text does not fit its
|
|
18171
|
+
// to save one that could not draw a component, or whose text does not fit its
|
|
18172
|
+
// boxes, without saying so, then save.
|
|
18029
18173
|
//
|
|
18030
18174
|
// The gates are the reason this sits between the collector and the renderers
|
|
18031
18175
|
// rather than in either of them. A deck with five blank pages is worse than no
|
|
@@ -18064,11 +18208,12 @@ class ReportExportService {
|
|
|
18064
18208
|
? this._run(report, listRows, build)
|
|
18065
18209
|
: this._buildAndSave(collected, build)))));
|
|
18066
18210
|
}
|
|
18067
|
-
// Builds the file, then saves it once the overflow
|
|
18211
|
+
// Builds the file, then saves it once the undrawn and overflow gates let it
|
|
18212
|
+
// through.
|
|
18068
18213
|
_buildAndSave(collected, build) {
|
|
18069
18214
|
return from(build(collected))
|
|
18070
|
-
.pipe(switchMap((built) => this.
|
|
18071
|
-
.pipe(switchMap(() => from(Promise.resolve(built.save()))))));
|
|
18215
|
+
.pipe(switchMap((built) => this._undrawnGate(built)
|
|
18216
|
+
.pipe(switchMap(() => this._overflowGate(built)), switchMap(() => from(Promise.resolve(built.save()))))));
|
|
18072
18217
|
}
|
|
18073
18218
|
// Resolves to DOWNLOAD when the export is whole (or the user accepted an
|
|
18074
18219
|
// incomplete one) and to RETRY when they want another attempt. Cancelling
|
|
@@ -18094,6 +18239,30 @@ class ReportExportService {
|
|
|
18094
18239
|
})
|
|
18095
18240
|
.pipe(catchError(() => EMPTY));
|
|
18096
18241
|
}
|
|
18242
|
+
// Resolves to DOWNLOAD when the build drew every component, or the user
|
|
18243
|
+
// accepted a file that marks the ones it could not in their place. There is
|
|
18244
|
+
// no Retry: a second build draws the same data the same way. Each component
|
|
18245
|
+
// is named by its page and its title (a text block by the start of its
|
|
18246
|
+
// words). Cancelling ends the export quietly, as at the failure gate, and
|
|
18247
|
+
// the built file is dropped unsaved.
|
|
18248
|
+
_undrawnGate(built) {
|
|
18249
|
+
if (!built.undrawn.length) {
|
|
18250
|
+
return of(DOWNLOAD);
|
|
18251
|
+
}
|
|
18252
|
+
const components = built.undrawn
|
|
18253
|
+
.map((undrawn) => `<strong>${escapeHtml(undrawn.page)}</strong>`
|
|
18254
|
+
+ (undrawn.name ? ` (“${escapeHtml(undrawn.name)}”)` : ''));
|
|
18255
|
+
return this._prompt.confirm({
|
|
18256
|
+
title: 'Some tiles could not be drawn',
|
|
18257
|
+
template: `Some tiles could not be drawn: ${components.join(', ')}. `
|
|
18258
|
+
+ 'The file says "Could not be drawn" in their place.',
|
|
18259
|
+
buttons: [
|
|
18260
|
+
{ label: 'Download Anyway', value: DOWNLOAD, color: 'primary' },
|
|
18261
|
+
{ label: 'Cancel', cancel: true, color: '' },
|
|
18262
|
+
],
|
|
18263
|
+
})
|
|
18264
|
+
.pipe(catchError(() => EMPTY));
|
|
18265
|
+
}
|
|
18097
18266
|
// Resolves to DOWNLOAD when every text block fits its box in the built file,
|
|
18098
18267
|
// or the user accepted one that does not. There is no Retry: the words are
|
|
18099
18268
|
// the report's own, so a second attempt would build the same file. Each block
|
|
@@ -18898,7 +19067,7 @@ class FsAiReportComponent {
|
|
|
18898
19067
|
ReportAiChatData,
|
|
18899
19068
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
18900
19069
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
18901
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19070
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-z76sZHp7.mjs').then((module) => module.default) }),
|
|
18902
19071
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
18903
19072
|
// presets; a preset only sets the days.
|
|
18904
19073
|
provideReportDatePicker(),
|
|
@@ -18922,7 +19091,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
18922
19091
|
ReportAiChatData,
|
|
18923
19092
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
18924
19093
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
18925
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19094
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-z76sZHp7.mjs').then((module) => module.default) }),
|
|
18926
19095
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
18927
19096
|
// presets; a preset only sets the days.
|
|
18928
19097
|
provideReportDatePicker(),
|
|
@@ -19015,4 +19184,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19015
19184
|
*/
|
|
19016
19185
|
|
|
19017
19186
|
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, ReportChangePolarity as r };
|
|
19018
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
19187
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-DYs6aHtD.mjs.map
|