@firestitch/report 18.18.1 → 18.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/app/reports/chart-axis-settings.d.ts +4 -0
  2. package/app/reports/components/report-component/report-component.component.d.ts +1 -0
  3. package/app/reports/dialogs/component-settings/component-settings.component.d.ts +24 -0
  4. package/app/reports/dialogs/global-settings/global-settings.component.d.ts +2 -6
  5. package/app/reports/enums/report-chart-date-label.enum.d.ts +2 -1
  6. package/app/reports/enums/report-chart-span-mode.enum.d.ts +5 -0
  7. package/app/reports/enums/report-chart-span-through.enum.d.ts +3 -0
  8. package/app/reports/format.d.ts +1 -0
  9. package/app/reports/interfaces/report-chart-axis-settings.interface.d.ts +6 -0
  10. package/app/reports/interfaces/report-chart-span.interface.d.ts +6 -0
  11. package/app/reports/interfaces/report.interface.d.ts +5 -0
  12. package/esm2022/app/reports/chart-axis-settings.mjs +24 -0
  13. package/esm2022/app/reports/components/report-component/report-component.component.mjs +27 -4
  14. package/esm2022/app/reports/dialogs/component-settings/component-settings.component.mjs +121 -16
  15. package/esm2022/app/reports/dialogs/global-settings/components/ai-knowledge/ai-knowledge.component.mjs +8 -6
  16. package/esm2022/app/reports/dialogs/global-settings/components/semantic-database/semantic-database.component.mjs +3 -3
  17. package/esm2022/app/reports/dialogs/global-settings/global-settings.component.mjs +32 -51
  18. package/esm2022/app/reports/dialogs/instruction-history/instruction-history.component.mjs +5 -5
  19. package/esm2022/app/reports/enums/report-chart-date-label.enum.mjs +5 -2
  20. package/esm2022/app/reports/enums/report-chart-span-mode.enum.mjs +10 -0
  21. package/esm2022/app/reports/enums/report-chart-span-through.enum.mjs +11 -0
  22. package/esm2022/app/reports/format.mjs +3 -1
  23. package/esm2022/app/reports/interfaces/report-chart-axis-settings.interface.mjs +2 -0
  24. package/esm2022/app/reports/interfaces/report-chart-span.interface.mjs +2 -0
  25. package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
  26. package/esm2022/app/reports/option-builder.mjs +34 -10
  27. package/esm2022/public_api.mjs +4 -1
  28. package/fesm2022/{firestitch-report-echarts-DrTDdBIP.mjs → firestitch-report-echarts-WOx5HoDM.mjs} +2 -2
  29. package/fesm2022/{firestitch-report-echarts-DrTDdBIP.mjs.map → firestitch-report-echarts-WOx5HoDM.mjs.map} +1 -1
  30. package/fesm2022/{firestitch-report-firestitch-report-Bgr_IprU.mjs → firestitch-report-firestitch-report-DztXL6p8.mjs} +286 -136
  31. package/fesm2022/firestitch-report-firestitch-report-DztXL6p8.mjs.map +1 -0
  32. package/fesm2022/firestitch-report.mjs +1 -1
  33. package/package.json +1 -1
  34. package/public_api.d.ts +3 -0
  35. package/app/reports/dialogs/global-settings/components/general/general.component.d.ts +0 -8
  36. package/esm2022/app/reports/dialogs/global-settings/components/general/general.component.mjs +0 -34
  37. package/fesm2022/firestitch-report-firestitch-report-Bgr_IprU.mjs.map +0 -1
@@ -3,7 +3,7 @@ import { inject, Injectable, signal, DestroyRef, Component, ChangeDetectionStrat
3
3
  import { takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop';
4
4
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
5
5
  import { MAT_DIALOG_DATA, MatDialogContent, MatDialogActions, MatDialogClose, MatDialog, MatDialogRef, MatDialogTitle } from '@angular/material/dialog';
6
- import * as i5$2 from '@angular/material/icon';
6
+ import * as i5 from '@angular/material/icon';
7
7
  import { MatIconModule, MatIcon } from '@angular/material/icon';
8
8
  import * as i1 from '@firestitch/chip';
9
9
  import { FsChipModule } from '@firestitch/chip';
@@ -27,35 +27,33 @@ import * as i6 from '@angular/material/tabs';
27
27
  import { MatTabsModule } from '@angular/material/tabs';
28
28
  import * as i2$1 from '@firestitch/dialog';
29
29
  import { FsDialogModule } from '@firestitch/dialog';
30
- import * as i2$2 from '@firestitch/form';
30
+ import * as i3 from '@firestitch/form';
31
31
  import { FsFormModule, FsFormDialogActionsComponent } from '@firestitch/form';
32
- import * as i5 from '@firestitch/tabs';
32
+ import * as i7 from '@firestitch/tabs';
33
33
  import { FsTabsModule } from '@firestitch/tabs';
34
34
  import { switchMap, tap, filter, map, take, catchError, shareReplay, scan, finalize, concatMap, toArray, mergeMap as mergeMap$1, takeUntil } from 'rxjs/operators';
35
35
  import * as i2 from '@firestitch/date';
36
36
  import { FsDateModule } from '@firestitch/date';
37
37
  import { FsMarkdownRendererComponent, FsMarkdownEditorComponent } from '@firestitch/markdown-editor';
38
- import { MatFormField, MatLabel, MatHint, MatSuffix } from '@angular/material/form-field';
39
- import { MatInput } from '@angular/material/input';
40
38
  import * as i1$2 from '@angular/material/expansion';
41
39
  import { MatExpansionModule } from '@angular/material/expansion';
42
- import * as i2$3 from '@angular/material/tooltip';
40
+ import * as i2$2 from '@angular/material/tooltip';
43
41
  import { MatTooltipModule, MatTooltip } from '@angular/material/tooltip';
44
42
  import { HttpContext, HttpErrorResponse } from '@angular/common/http';
45
43
  import { MatCheckbox } from '@angular/material/checkbox';
46
44
  import { MatProgressSpinner } from '@angular/material/progress-spinner';
47
- import * as i5$1 from '@firestitch/label';
45
+ import * as i6$1 from '@firestitch/label';
48
46
  import { FsLabelModule } from '@firestitch/label';
49
- import * as i2$4 from '@firestitch/menu';
47
+ import * as i2$3 from '@firestitch/menu';
50
48
  import { FsMenuModule } from '@firestitch/menu';
51
49
  import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
52
50
  import { FsAiChatData, FsAiChatComponent } from '@firestitch/ai';
53
51
  import * as i1$5 from '@angular/cdk/drag-drop';
54
52
  import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
55
- import * as i3 from '@firestitch/zoom-pan';
53
+ import * as i3$1 from '@firestitch/zoom-pan';
56
54
  import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
57
55
  import { GridFieldType, GridGranularity, canonicalGridLayout, convertDateFilter, GridNoteCode, GridControlState, GridFormat, GridUnits, FsGridComponent, FsGridCellDirective, GridRowKind, headingLabel, readGridTable } from '@firestitch/grid';
58
- import { parseISO, isValid, addDays, min, max, startOfDay, isLastDayOfMonth, endOfMonth, addMonths, addYears, addQuarters, addWeeks, formatISO, format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek } from 'date-fns';
56
+ import { parseISO, isValid, addDays, min, max, startOfDay, isLastDayOfMonth, endOfMonth, addMonths, addYears, addQuarters, addWeeks, formatISO, endOfYear, endOfQuarter, endOfWeek, format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek } from 'date-fns';
59
57
  import { toZonedTime } from 'date-fns-tz';
60
58
  import { FsPopoverDirective } from '@firestitch/popover';
61
59
  import { NgTemplateOutlet, DOCUMENT } from '@angular/common';
@@ -63,14 +61,16 @@ import * as i1$4 from '@firestitch/list';
63
61
  import { FsListComponent, PaginationStrategy, FsListModule } from '@firestitch/list';
64
62
  import { DomSanitizer } from '@angular/platform-browser';
65
63
  import { Plugin, FsHtmlEditorComponent } from '@firestitch/html-editor';
64
+ import { MatFormField, MatHint, MatLabel, MatSuffix } from '@angular/material/form-field';
65
+ import { MatInput } from '@angular/material/input';
66
66
  import { MatOption, MatSelect } from '@angular/material/select';
67
67
  import { MatSlideToggle } from '@angular/material/slide-toggle';
68
- import * as i3$1 from '@firestitch/mask';
68
+ import * as i2$4 from '@firestitch/datepicker';
69
+ import { FsDatePickerModule, FS_DATEPICKER_CONFIG_DEFAULT, FS_DATEPICKER_CONFIG } from '@firestitch/datepicker';
70
+ import * as i4$3 from '@firestitch/mask';
69
71
  import { FsMaskModule } from '@firestitch/mask';
70
72
  import * as i10 from '@firestitch/autocomplete-chips';
71
73
  import { FsAutocompleteChipsModule } from '@firestitch/autocomplete-chips';
72
- import * as i9 from '@firestitch/datepicker';
73
- import { FsDatePickerModule, FS_DATEPICKER_CONFIG_DEFAULT, FS_DATEPICKER_CONFIG } from '@firestitch/datepicker';
74
74
  import { MatOption as MatOption$1 } from '@angular/material/core';
75
75
  import { guid } from '@firestitch/common';
76
76
 
@@ -631,7 +631,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
631
631
  }]
632
632
  }] });
633
633
 
634
- // The AI Knowledge document's version history (append-only), each version
634
+ // The Knowledge document's version history (append-only), each version
635
635
  // previewed and restorable — opened from the Report Settings footer's View
636
636
  // History button. Restores happen here (confirm, then flip the current flag —
637
637
  // no new version is created); the opener refreshes its instructions after this
@@ -647,7 +647,7 @@ class InstructionHistoryComponent {
647
647
  restore(version) {
648
648
  this._prompt.confirm({
649
649
  title: 'Restore version',
650
- template: 'Make this version the current AI knowledge? The newer versions stay in the history.',
650
+ template: 'Make this version the current knowledge? The newer versions stay in the history.',
651
651
  commitLabel: 'Restore',
652
652
  })
653
653
  .pipe(switchMap(() => this._reportData.restoreInstruction(version.id)), takeUntilDestroyed(this._destroyRef))
@@ -657,7 +657,7 @@ class InstructionHistoryComponent {
657
657
  });
658
658
  }
659
659
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InstructionHistoryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
660
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: InstructionHistoryComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<fs-dialog>\n <fs-dialog-title>\n AI Knowledge History\n </fs-dialog-title>\n <mat-dialog-content>\n @if (versions().length) {\n <div class=\"versions\">\n @for (version of versions(); track version.id) {\n <div class=\"version\">\n <div class=\"version-header\">\n <fs-date\n [date]=\"version.createDate\"\n format=\"date-time\">\n </fs-date>\n @if (version.accountName) {\n <span class=\"author\">{{ version.accountName }}</span>\n }\n <span class=\"spacer\"></span>\n @if (version.current) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"'#2e7d32'\"\n color=\"#ffffff\">\n Current\n </fs-chip>\n } @else {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"restore(version)\">\n Restore\n </button>\n }\n </div>\n <div class=\"version-preview\">\n <fs-markdown-renderer [markdown]=\"version.content\"></fs-markdown-renderer>\n </div>\n </div>\n }\n </div>\n } @else {\n <fs-message-info>\n No versions yet \u2014 every save of the AI Knowledge document lands here, and\n any version can be restored.\n </fs-message-info>\n }\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Close\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".version{border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:12px}.version+.version{margin-top:12px}.version .version-header{display:flex;align-items:center;gap:8px}.version .version-header .author{color:#0009}.version .version-header .spacer{flex:1}.version .version-preview{margin-top:8px;max-height:200px;overflow-y:auto;border-top:1px solid rgba(0,0,0,.08);padding-top:8px}\n"], dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i2$1.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "component", type: FsMarkdownRendererComponent, selector: "fs-markdown-renderer", inputs: ["config", "markdown"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageInfoComponent, selector: "fs-message-info" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
660
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: InstructionHistoryComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<fs-dialog>\n <fs-dialog-title>\n Knowledge History\n </fs-dialog-title>\n <mat-dialog-content>\n @if (versions().length) {\n <div class=\"versions\">\n @for (version of versions(); track version.id) {\n <div class=\"version\">\n <div class=\"version-header\">\n <fs-date\n [date]=\"version.createDate\"\n format=\"date-time\">\n </fs-date>\n @if (version.accountName) {\n <span class=\"author\">{{ version.accountName }}</span>\n }\n <span class=\"spacer\"></span>\n @if (version.current) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"'#2e7d32'\"\n color=\"#ffffff\">\n Current\n </fs-chip>\n } @else {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"restore(version)\">\n Restore\n </button>\n }\n </div>\n <div class=\"version-preview\">\n <fs-markdown-renderer [markdown]=\"version.content\"></fs-markdown-renderer>\n </div>\n </div>\n }\n </div>\n } @else {\n <fs-message-info>\n No versions yet \u2014 every save of the Knowledge document lands here, and\n any version can be restored.\n </fs-message-info>\n }\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Close\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".version{border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:12px}.version+.version{margin-top:12px}.version .version-header{display:flex;align-items:center;gap:8px}.version .version-header .author{color:#0009}.version .version-header .spacer{flex:1}.version .version-preview{margin-top:8px;max-height:200px;overflow-y:auto;border-top:1px solid rgba(0,0,0,.08);padding-top:8px}\n"], dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i2$1.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "component", type: FsMarkdownRendererComponent, selector: "fs-markdown-renderer", inputs: ["config", "markdown"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageInfoComponent, selector: "fs-message-info" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
661
661
  }
662
662
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: InstructionHistoryComponent, decorators: [{
663
663
  type: Component,
@@ -671,12 +671,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
671
671
  FsDialogModule,
672
672
  FsMarkdownRendererComponent,
673
673
  FsMessageModule,
674
- ], template: "<fs-dialog>\n <fs-dialog-title>\n AI Knowledge History\n </fs-dialog-title>\n <mat-dialog-content>\n @if (versions().length) {\n <div class=\"versions\">\n @for (version of versions(); track version.id) {\n <div class=\"version\">\n <div class=\"version-header\">\n <fs-date\n [date]=\"version.createDate\"\n format=\"date-time\">\n </fs-date>\n @if (version.accountName) {\n <span class=\"author\">{{ version.accountName }}</span>\n }\n <span class=\"spacer\"></span>\n @if (version.current) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"'#2e7d32'\"\n color=\"#ffffff\">\n Current\n </fs-chip>\n } @else {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"restore(version)\">\n Restore\n </button>\n }\n </div>\n <div class=\"version-preview\">\n <fs-markdown-renderer [markdown]=\"version.content\"></fs-markdown-renderer>\n </div>\n </div>\n }\n </div>\n } @else {\n <fs-message-info>\n No versions yet \u2014 every save of the AI Knowledge document lands here, and\n any version can be restored.\n </fs-message-info>\n }\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Close\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".version{border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:12px}.version+.version{margin-top:12px}.version .version-header{display:flex;align-items:center;gap:8px}.version .version-header .author{color:#0009}.version .version-header .spacer{flex:1}.version .version-preview{margin-top:8px;max-height:200px;overflow-y:auto;border-top:1px solid rgba(0,0,0,.08);padding-top:8px}\n"] }]
674
+ ], template: "<fs-dialog>\n <fs-dialog-title>\n Knowledge History\n </fs-dialog-title>\n <mat-dialog-content>\n @if (versions().length) {\n <div class=\"versions\">\n @for (version of versions(); track version.id) {\n <div class=\"version\">\n <div class=\"version-header\">\n <fs-date\n [date]=\"version.createDate\"\n format=\"date-time\">\n </fs-date>\n @if (version.accountName) {\n <span class=\"author\">{{ version.accountName }}</span>\n }\n <span class=\"spacer\"></span>\n @if (version.current) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"'#2e7d32'\"\n color=\"#ffffff\">\n Current\n </fs-chip>\n } @else {\n <button\n type=\"button\"\n mat-stroked-button\n (click)=\"restore(version)\">\n Restore\n </button>\n }\n </div>\n <div class=\"version-preview\">\n <fs-markdown-renderer [markdown]=\"version.content\"></fs-markdown-renderer>\n </div>\n </div>\n }\n </div>\n } @else {\n <fs-message-info>\n No versions yet \u2014 every save of the Knowledge document lands here, and\n any version can be restored.\n </fs-message-info>\n }\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Close\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".version{border:1px solid rgba(0,0,0,.12);border-radius:6px;padding:12px}.version+.version{margin-top:12px}.version .version-header{display:flex;align-items:center;gap:8px}.version .version-header .author{color:#0009}.version .version-header .spacer{flex:1}.version .version-preview{margin-top:8px;max-height:200px;overflow-y:auto;border-top:1px solid rgba(0,0,0,.08);padding-top:8px}\n"] }]
675
675
  }] });
676
676
 
677
- // The AI Knowledge tab: the operator-authored markdown document appended to the
678
- // report agent's system instructions on every chat turn — the lever for
679
- // terminology, business rules and special cases no code deploy can cover.
677
+ // The Knowledge tab: the operator-authored markdown document every report build
678
+ // reads — appended to the in-app agent's system instructions on every chat turn,
679
+ // and read by a client over MCP (ReadReportKnowledgeOperation), so it is kept
680
+ // up to date here even when the report chat is off. The lever for terminology,
681
+ // business rules and special cases no code deploy can cover.
680
682
  //
681
683
  // A field-level child of the settings dialog form: the editor registers with
682
684
  // the parent <form fsForm> via the ControlContainer view provider, so the
@@ -687,7 +689,7 @@ class AiKnowledgeComponent {
687
689
  content = model('');
688
690
  config = input.required();
689
691
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AiKnowledgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
690
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: AiKnowledgeComponent, isStandalone: true, selector: "fs-ai-report-global-settings-ai-knowledge", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { content: "contentChange" }, ngImport: i0, template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n This document is given to the report AI on every chat message. Use it for\n terminology, business rules and special cases the AI can't work out from the\n data on its own \u2014 it takes effect immediately, no deploy needed.\n </p>\n <fs-markdown-editor\n [ngModel]=\"content()\"\n (ngModelChange)=\"content.set($event)\"\n [config]=\"config()\"\n name=\"content\">\n </fs-markdown-editor>\n</div>\n", styles: [".tab-body{padding-top:16px}.hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}fs-markdown-editor{display:block;min-height:320px}\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: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$2.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "component", type: FsMarkdownEditorComponent, selector: "fs-markdown-editor", inputs: ["config", "disabled"] }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
692
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: AiKnowledgeComponent, isStandalone: true, selector: "fs-ai-report-global-settings-ai-knowledge", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { content: "contentChange" }, ngImport: i0, template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n Business terms, rules and special cases every report is built with, in the\n app or by Claude over MCP. Takes effect immediately.\n </p>\n <fs-markdown-editor\n [ngModel]=\"content()\"\n (ngModelChange)=\"content.set($event)\"\n [config]=\"config()\"\n name=\"content\">\n </fs-markdown-editor>\n</div>\n", styles: [".tab-body{padding-top:16px}.hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}fs-markdown-editor{display:block;min-height:320px}\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: "ngmodule", type: FsFormModule }, { kind: "directive", type: i3.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "component", type: FsMarkdownEditorComponent, selector: "fs-markdown-editor", inputs: ["config", "disabled"] }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
691
693
  }
692
694
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AiKnowledgeComponent, decorators: [{
693
695
  type: Component,
@@ -695,33 +697,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
695
697
  FormsModule,
696
698
  FsFormModule,
697
699
  FsMarkdownEditorComponent,
698
- ], template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n This document is given to the report AI on every chat message. Use it for\n terminology, business rules and special cases the AI can't work out from the\n data on its own \u2014 it takes effect immediately, no deploy needed.\n </p>\n <fs-markdown-editor\n [ngModel]=\"content()\"\n (ngModelChange)=\"content.set($event)\"\n [config]=\"config()\"\n name=\"content\">\n </fs-markdown-editor>\n</div>\n", styles: [".tab-body{padding-top:16px}.hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}fs-markdown-editor{display:block;min-height:320px}\n"] }]
699
- }] });
700
-
701
- // The General tab: report-module settings that aren't the AI knowledge document.
702
- // Currently just the semantic-build concurrency — how many summary calls the
703
- // build runs at once. A field-level child of the settings dialog form (its input
704
- // registers with the parent <form fsForm> via the ControlContainer view
705
- // provider), so the dialog's Save button submits and validates it. The value
706
- // itself is held on the shell (bound through `concurrency`), and the safe band
707
- // (`min`/`max`) comes from the backend so the field can't be set out of range.
708
- class GeneralComponent {
709
- concurrency = model(0);
710
- min = input.required();
711
- max = input.required();
712
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GeneralComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
713
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: GeneralComponent, isStandalone: true, selector: "fs-ai-report-global-settings-general", inputs: { concurrency: { classPropertyName: "concurrency", publicName: "concurrency", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: true, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { concurrency: "concurrencyChange" }, ngImport: i0, template: "<div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>Semantic build concurrency</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"concurrency()\"\n (ngModelChange)=\"concurrency.set($event)\"\n name=\"semanticBuildConcurrency\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"min()\"\n [fsFormMax]=\"max()\">\n <mat-hint>\n How many AI calls the semantic-database build runs at once ({{ min() }}\u2013{{ max() }}).\n Higher is faster but more likely to hit the AI provider's rate limit. Default is 8.\n </mat-hint>\n </mat-form-field>\n</div>\n", styles: [".tab-body{padding-top:16px}mat-form-field{display:block;max-width:340px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { 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: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$2.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$2.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$2.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "directive", type: i2$2.FsFormIntegerDirective, selector: "[fsFormInteger]", inputs: ["fsFormInteger", "fsFormIntegerMessage"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
714
- }
715
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GeneralComponent, decorators: [{
716
- type: Component,
717
- args: [{ selector: 'fs-ai-report-global-settings-general', viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
718
- FormsModule,
719
- FsFormModule,
720
- MatFormField,
721
- MatLabel,
722
- MatHint,
723
- MatInput,
724
- ], template: "<div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>Semantic build concurrency</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"concurrency()\"\n (ngModelChange)=\"concurrency.set($event)\"\n name=\"semanticBuildConcurrency\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"min()\"\n [fsFormMax]=\"max()\">\n <mat-hint>\n How many AI calls the semantic-database build runs at once ({{ min() }}\u2013{{ max() }}).\n Higher is faster but more likely to hit the AI provider's rate limit. Default is 8.\n </mat-hint>\n </mat-form-field>\n</div>\n", styles: [".tab-body{padding-top:16px}mat-form-field{display:block;max-width:340px}\n"] }]
700
+ ], template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n Business terms, rules and special cases every report is built with, in the\n app or by Claude over MCP. Takes effect immediately.\n </p>\n <fs-markdown-editor\n [ngModel]=\"content()\"\n (ngModelChange)=\"content.set($event)\"\n [config]=\"config()\"\n name=\"content\">\n </fs-markdown-editor>\n</div>\n", styles: [".tab-body{padding-top:16px}.hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}fs-markdown-editor{display:block;min-height:320px}\n"] }]
725
701
  }] });
726
702
 
727
703
  // One group of a unit's AI-classified cached code functions — an entity's model
@@ -1134,7 +1110,7 @@ class SemanticDatabaseComponent {
1134
1110
  }), takeUntilDestroyed(this._destroyRef));
1135
1111
  }
1136
1112
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SemanticDatabaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1137
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SemanticDatabaseComponent, isStandalone: true, selector: "fs-ai-report-global-settings-semantic-database", ngImport: i0, template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n The semantic database is what the report AI uses to understand your data \u2014\n tables, relationships and the calculations in code. Rebuild it after\n significant application updates. The build runs in the background; progress\n appears in the dock at the bottom right and this dialog can be closed while\n it runs.\n </p>\n\n <!-- A failed status load settles the skeleton too, so the tab says what\n happened instead of loading forever. -->\n @if (statusError()) {\n <fs-message-warning>\n The build status could not be loaded, so the last build and its coverage\n can't be shown. You can still run a build.\n </fs-message-warning>\n }\n\n <div *fsSkeleton=\"status() || statusError()\">\n @if (status(); as buildStatus) {\n <div class=\"build-status\">\n @if (buildStatus.detail.generatedAt) {\n <div class=\"stat\">\n <span class=\"label\">Last built</span>\n <fs-date\n [date]=\"buildStatus.detail.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Coverage</span>\n <span>{{ coverage() }}</span>\n </div>\n } @else {\n <fs-message-warning>\n The semantic database has not been built yet \u2014 the report AI can't\n answer data questions until it is. Run the first build.\n </fs-message-warning>\n }\n @if (buildStatus.build) {\n <div class=\"stat\">\n <span class=\"label\">Last build run</span>\n <span>\n {{ buildStateLabel() }}\n @if (buildStatus.build.message) {\n \u2014 {{ buildStatus.build.message }}\n }\n </span>\n </div>\n }\n\n <!-- Everything above is true about what was published, and on its own it\n reads as a finished build: the structural half publishes BEFORE the\n AI function pass, so a run that died in that pass still stamps a\n date and still fills in the whole coverage line. This is the only\n line that says the build never got to the end. -->\n @if (buildStatus.detail.generatedAt && !buildComplete()) {\n <fs-message-warning>\n The last build did not finish, so this semantic database is incomplete \u2014\n {{ incompleteEntityCount() }} of {{ buildStatus.detail.entityCount }}\n entities have no code-function knowledge. The report AI answers from what\n is here and cannot see the rest. Rebuild to complete it.\n </fs-message-warning>\n }\n </div>\n\n <!-- Everything the build read, in one list: entities (a table + a model +\n a handler) and standalone code classes (domain logic with no table).\n The AI reads code from both, so splitting them into two sections\n would hide half of what was built. Each row drills into its shape,\n enum values, computed fields and cached function counts \u2014 the counts\n link into the functions dialog. -->\n @if (units().length) {\n <h2>Included ({{ units().length }})</h2>\n <div class=\"subheading\">\n Everything the report AI was built from. Entities can be queried;\n code classes are logic it reads and translates into SQL.\n </div>\n\n <input\n class=\"unit-filter\"\n type=\"search\"\n placeholder=\"Filter by name, class or key\u2026\"\n [value]=\"filter()\"\n (input)=\"setFilter($any($event.target).value)\">\n\n <mat-accordion class=\"entities\">\n @for (unit of filteredUnits(); track unit.key) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>{{ unit.name }}</mat-panel-title>\n <mat-panel-description>\n <div class=\"stage-chips\">\n @if (!unit.hasTable) {\n <fs-chip size=\"tiny\" matTooltip=\"Domain logic with no table \u2014 read, never queried\">\n code\n </fs-chip>\n }\n <!-- Silence means built. A chip here means an interrupted\n build left this row owing content. -->\n @for (stage of pendingStages(unit); track stage) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"pendingColor()\"\n color=\"#ffffff\"\n matTooltip=\"Needs building\">\n {{ stage }}\n </fs-chip>\n }\n </div>\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"entity-detail\">\n @if (unit.model) {\n <div class=\"stat\">\n <span class=\"label\">Class</span>\n <span>{{ unit.model }}</span>\n </div>\n }\n @if (unit.entity; as entity) {\n <div class=\"stat\">\n <span class=\"label\">Table</span>\n <span>{{ entity.table }}</span>\n </div>\n <!-- The handle the report AI addresses this entity by in\n describe_entity / compile_query. -->\n <div class=\"stat\">\n <span class=\"label\">Entity key</span>\n <span>{{ entity.key }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Columns</span>\n <span>{{ entity.columns }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Relationships</span>\n <span>{{ entity.relationships }}</span>\n </div>\n @if (entity.enums.length) {\n <div class=\"stat\">\n <span class=\"label\">Enumeration values</span>\n <div class=\"pair-list\">\n @for (enumColumn of entity.enums; track enumColumn.column) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ enumColumn.column }}</span>\n <span>{{ enumColumn.values.join(', ') }}</span>\n </div>\n }\n </div>\n </div>\n }\n @if (entity.computedFields.length) {\n <div class=\"stat\">\n <span class=\"label\">Computed fields</span>\n <span>{{ entity.computedFields.join(', ') }}</span>\n </div>\n }\n <div class=\"stat\">\n <span class=\"label\">Model functions</span>\n @if (entity.modelFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'model')\">\n {{ entity.modelFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n <div class=\"stat\">\n <span class=\"label\">Handler functions</span>\n @if (entity.handlerFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'handler')\">\n {{ entity.handlerFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n } @else {\n <div class=\"stat\">\n <span class=\"label\">Code functions</span>\n @if (unit.functionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'class')\">\n {{ unit.functionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n\n @if (!filteredUnits().length) {\n <div class=\"subheading\">Nothing matches \u201C{{ filter() }}\u201D.</div>\n }\n }\n\n <!-- The other half of the picture: tables the AI can't reach. Excluded\n ones have no model; unlinked junctions have a composite primary key\n but were folded into no relationship, so nothing can navigate to\n them. Both are invisible by omission \u2014 no error, just a capability\n that silently isn't there. Collapsed by default: the excluded list is\n long and mostly framework plumbing. -->\n @if (hasCatalog()) {\n <h2>Not in the semantic database</h2>\n <div class=\"subheading\">\n Tables the report AI can't see. Give one a model to promote it to an\n entity, then rebuild.\n </div>\n <mat-accordion class=\"catalog\">\n @if (excludedTables().length) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n Tables excluded ({{ excludedTables().length }})\n </mat-panel-title>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"pair-list\">\n @for (table of excludedTables(); track table.table) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ table.table }}</span>\n <span>{{ table.reason }}</span>\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n @if (unlinkedJunctions().length) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n Junctions not linked ({{ unlinkedJunctions().length }})\n </mat-panel-title>\n </mat-expansion-panel-header>\n\n <ng-template matExpansionPanelContent>\n <p class=\"small\">\n Link tables the build couldn't fold into a relationship \u2014 so\n neither they nor what they link can be reported on.\n </p>\n <div class=\"pair-list\">\n @for (table of unlinkedJunctions(); track table.table) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ table.table }}</span>\n <span>{{ table.reason }}</span>\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n }\n }\n </div>\n</div>\n", styles: [".hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}.stat{display:flex;align-items:baseline;gap:12px}.stat>.label{flex:0 0 150px;white-space:nowrap;color:#0009}.build-status{display:flex;flex-direction:column;gap:8px}.pair-list{display:flex;flex-direction:column;gap:3px}.pair-row{display:flex;gap:12px}.pair-name{flex:0 0 140px;white-space:nowrap;color:#0009}.unit-filter{width:100%;box-sizing:border-box;margin:8px 0 4px;padding:6px 10px;border:1px solid rgba(0,0,0,.2);border-radius:4px;font:inherit}.unit-filter:focus{outline:none;border-color:#00000080}.entities{display:block;padding:3px}.entities mat-panel-description{justify-content:flex-end;margin-right:8px}.entities .stage-chips{display:flex;gap:4px}.entities .entity-detail{display:flex;flex-direction:column;gap:8px}.catalog{display:block;padding:3px}.catalog .pair-name{flex-basis:220px}\n"], dependencies: [{ kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i1$2.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i1$2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i1$2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i1$2.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i1$2.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "directive", type: i1$2.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageWarningComponent, selector: "fs-message-warning" }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1113
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SemanticDatabaseComponent, isStandalone: true, selector: "fs-ai-report-global-settings-semantic-database", ngImport: i0, template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n The semantic database describes your data for reports \u2014 tables,\n relationships and the calculations in code. Reports are built from it and\n read it every time they open, so rebuild it after significant application\n updates. The build runs in the background; progress appears in the dock at\n the bottom right and this dialog can be closed while it runs.\n </p>\n\n <!-- A failed status load settles the skeleton too, so the tab says what\n happened instead of loading forever. -->\n @if (statusError()) {\n <fs-message-warning>\n The build status could not be loaded, so the last build and its coverage\n can't be shown. You can still run a build.\n </fs-message-warning>\n }\n\n <div *fsSkeleton=\"status() || statusError()\">\n @if (status(); as buildStatus) {\n <div class=\"build-status\">\n @if (buildStatus.detail.generatedAt) {\n <div class=\"stat\">\n <span class=\"label\">Last built</span>\n <fs-date\n [date]=\"buildStatus.detail.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Coverage</span>\n <span>{{ coverage() }}</span>\n </div>\n } @else {\n <fs-message-warning>\n The semantic database has not been built yet \u2014 reports can't be\n built or show their data until it is. Run the first build.\n </fs-message-warning>\n }\n @if (buildStatus.build) {\n <div class=\"stat\">\n <span class=\"label\">Last build run</span>\n <span>\n {{ buildStateLabel() }}\n @if (buildStatus.build.message) {\n \u2014 {{ buildStatus.build.message }}\n }\n </span>\n </div>\n }\n\n <!-- Everything above is true about what was published, and on its own it\n reads as a finished build: the structural half publishes BEFORE the\n AI function pass, so a run that died in that pass still stamps a\n date and still fills in the whole coverage line. This is the only\n line that says the build never got to the end. -->\n @if (buildStatus.detail.generatedAt && !buildComplete()) {\n <fs-message-warning>\n The last build did not finish, so this semantic database is incomplete \u2014\n {{ incompleteEntityCount() }} of {{ buildStatus.detail.entityCount }}\n entities have no code-function knowledge. Reports are built from what\n is here and cannot use the rest. Rebuild to complete it.\n </fs-message-warning>\n }\n </div>\n\n <!-- Everything the build read, in one list: entities (a table + a model +\n a handler) and standalone code classes (domain logic with no table).\n The AI reads code from both, so splitting them into two sections\n would hide half of what was built. Each row drills into its shape,\n enum values, computed fields and cached function counts \u2014 the counts\n link into the functions dialog. -->\n @if (units().length) {\n <h2>Included ({{ units().length }})</h2>\n <div class=\"subheading\">\n Everything in the semantic database. Entities can be queried; code\n classes are logic that is read and translated into SQL when a report\n is built.\n </div>\n\n <input\n class=\"unit-filter\"\n type=\"search\"\n placeholder=\"Filter by name, class or key\u2026\"\n [value]=\"filter()\"\n (input)=\"setFilter($any($event.target).value)\">\n\n <mat-accordion class=\"entities\">\n @for (unit of filteredUnits(); track unit.key) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>{{ unit.name }}</mat-panel-title>\n <mat-panel-description>\n <div class=\"stage-chips\">\n @if (!unit.hasTable) {\n <fs-chip size=\"tiny\" matTooltip=\"Domain logic with no table \u2014 read, never queried\">\n code\n </fs-chip>\n }\n <!-- Silence means built. A chip here means an interrupted\n build left this row owing content. -->\n @for (stage of pendingStages(unit); track stage) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"pendingColor()\"\n color=\"#ffffff\"\n matTooltip=\"Needs building\">\n {{ stage }}\n </fs-chip>\n }\n </div>\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"entity-detail\">\n @if (unit.model) {\n <div class=\"stat\">\n <span class=\"label\">Class</span>\n <span>{{ unit.model }}</span>\n </div>\n }\n @if (unit.entity; as entity) {\n <div class=\"stat\">\n <span class=\"label\">Table</span>\n <span>{{ entity.table }}</span>\n </div>\n <!-- The handle the report AI addresses this entity by in\n describe_entity / compile_query. -->\n <div class=\"stat\">\n <span class=\"label\">Entity key</span>\n <span>{{ entity.key }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Columns</span>\n <span>{{ entity.columns }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Relationships</span>\n <span>{{ entity.relationships }}</span>\n </div>\n @if (entity.enums.length) {\n <div class=\"stat\">\n <span class=\"label\">Enumeration values</span>\n <div class=\"pair-list\">\n @for (enumColumn of entity.enums; track enumColumn.column) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ enumColumn.column }}</span>\n <span>{{ enumColumn.values.join(', ') }}</span>\n </div>\n }\n </div>\n </div>\n }\n @if (entity.computedFields.length) {\n <div class=\"stat\">\n <span class=\"label\">Computed fields</span>\n <span>{{ entity.computedFields.join(', ') }}</span>\n </div>\n }\n <div class=\"stat\">\n <span class=\"label\">Model functions</span>\n @if (entity.modelFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'model')\">\n {{ entity.modelFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n <div class=\"stat\">\n <span class=\"label\">Handler functions</span>\n @if (entity.handlerFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'handler')\">\n {{ entity.handlerFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n } @else {\n <div class=\"stat\">\n <span class=\"label\">Code functions</span>\n @if (unit.functionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'class')\">\n {{ unit.functionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n\n @if (!filteredUnits().length) {\n <div class=\"subheading\">Nothing matches \u201C{{ filter() }}\u201D.</div>\n }\n }\n\n <!-- The other half of the picture: tables the AI can't reach. Excluded\n ones have no model; unlinked junctions have a composite primary key\n but were folded into no relationship, so nothing can navigate to\n them. Both are invisible by omission \u2014 no error, just a capability\n that silently isn't there. Collapsed by default: the excluded list is\n long and mostly framework plumbing. -->\n @if (hasCatalog()) {\n <h2>Not in the semantic database</h2>\n <div class=\"subheading\">\n Tables reports can't use. Give one a model to promote it to an\n entity, then rebuild.\n </div>\n <mat-accordion class=\"catalog\">\n @if (excludedTables().length) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n Tables excluded ({{ excludedTables().length }})\n </mat-panel-title>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"pair-list\">\n @for (table of excludedTables(); track table.table) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ table.table }}</span>\n <span>{{ table.reason }}</span>\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n @if (unlinkedJunctions().length) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n Junctions not linked ({{ unlinkedJunctions().length }})\n </mat-panel-title>\n </mat-expansion-panel-header>\n\n <ng-template matExpansionPanelContent>\n <p class=\"small\">\n Link tables the build couldn't fold into a relationship \u2014 so\n neither they nor what they link can be reported on.\n </p>\n <div class=\"pair-list\">\n @for (table of unlinkedJunctions(); track table.table) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ table.table }}</span>\n <span>{{ table.reason }}</span>\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n }\n }\n </div>\n</div>\n", styles: [".hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}.stat{display:flex;align-items:baseline;gap:12px}.stat>.label{flex:0 0 150px;white-space:nowrap;color:#0009}.build-status{display:flex;flex-direction:column;gap:8px}.pair-list{display:flex;flex-direction:column;gap:3px}.pair-row{display:flex;gap:12px}.pair-name{flex:0 0 140px;white-space:nowrap;color:#0009}.unit-filter{width:100%;box-sizing:border-box;margin:8px 0 4px;padding:6px 10px;border:1px solid rgba(0,0,0,.2);border-radius:4px;font:inherit}.unit-filter:focus{outline:none;border-color:#00000080}.entities{display:block;padding:3px}.entities mat-panel-description{justify-content:flex-end;margin-right:8px}.entities .stage-chips{display:flex;gap:4px}.entities .entity-detail{display:flex;flex-direction:column;gap:8px}.catalog{display:block;padding:3px}.catalog .pair-name{flex-basis:220px}\n"], dependencies: [{ kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i1$2.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i1$2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i1$2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i1$2.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i1$2.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "directive", type: i1$2.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i2.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageWarningComponent, selector: "fs-message-warning" }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1138
1114
  }
1139
1115
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SemanticDatabaseComponent, decorators: [{
1140
1116
  type: Component,
@@ -1145,11 +1121,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
1145
1121
  FsDateModule,
1146
1122
  FsMessageModule,
1147
1123
  FsSkeletonModule,
1148
- ], template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n The semantic database is what the report AI uses to understand your data \u2014\n tables, relationships and the calculations in code. Rebuild it after\n significant application updates. The build runs in the background; progress\n appears in the dock at the bottom right and this dialog can be closed while\n it runs.\n </p>\n\n <!-- A failed status load settles the skeleton too, so the tab says what\n happened instead of loading forever. -->\n @if (statusError()) {\n <fs-message-warning>\n The build status could not be loaded, so the last build and its coverage\n can't be shown. You can still run a build.\n </fs-message-warning>\n }\n\n <div *fsSkeleton=\"status() || statusError()\">\n @if (status(); as buildStatus) {\n <div class=\"build-status\">\n @if (buildStatus.detail.generatedAt) {\n <div class=\"stat\">\n <span class=\"label\">Last built</span>\n <fs-date\n [date]=\"buildStatus.detail.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Coverage</span>\n <span>{{ coverage() }}</span>\n </div>\n } @else {\n <fs-message-warning>\n The semantic database has not been built yet \u2014 the report AI can't\n answer data questions until it is. Run the first build.\n </fs-message-warning>\n }\n @if (buildStatus.build) {\n <div class=\"stat\">\n <span class=\"label\">Last build run</span>\n <span>\n {{ buildStateLabel() }}\n @if (buildStatus.build.message) {\n \u2014 {{ buildStatus.build.message }}\n }\n </span>\n </div>\n }\n\n <!-- Everything above is true about what was published, and on its own it\n reads as a finished build: the structural half publishes BEFORE the\n AI function pass, so a run that died in that pass still stamps a\n date and still fills in the whole coverage line. This is the only\n line that says the build never got to the end. -->\n @if (buildStatus.detail.generatedAt && !buildComplete()) {\n <fs-message-warning>\n The last build did not finish, so this semantic database is incomplete \u2014\n {{ incompleteEntityCount() }} of {{ buildStatus.detail.entityCount }}\n entities have no code-function knowledge. The report AI answers from what\n is here and cannot see the rest. Rebuild to complete it.\n </fs-message-warning>\n }\n </div>\n\n <!-- Everything the build read, in one list: entities (a table + a model +\n a handler) and standalone code classes (domain logic with no table).\n The AI reads code from both, so splitting them into two sections\n would hide half of what was built. Each row drills into its shape,\n enum values, computed fields and cached function counts \u2014 the counts\n link into the functions dialog. -->\n @if (units().length) {\n <h2>Included ({{ units().length }})</h2>\n <div class=\"subheading\">\n Everything the report AI was built from. Entities can be queried;\n code classes are logic it reads and translates into SQL.\n </div>\n\n <input\n class=\"unit-filter\"\n type=\"search\"\n placeholder=\"Filter by name, class or key\u2026\"\n [value]=\"filter()\"\n (input)=\"setFilter($any($event.target).value)\">\n\n <mat-accordion class=\"entities\">\n @for (unit of filteredUnits(); track unit.key) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>{{ unit.name }}</mat-panel-title>\n <mat-panel-description>\n <div class=\"stage-chips\">\n @if (!unit.hasTable) {\n <fs-chip size=\"tiny\" matTooltip=\"Domain logic with no table \u2014 read, never queried\">\n code\n </fs-chip>\n }\n <!-- Silence means built. A chip here means an interrupted\n build left this row owing content. -->\n @for (stage of pendingStages(unit); track stage) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"pendingColor()\"\n color=\"#ffffff\"\n matTooltip=\"Needs building\">\n {{ stage }}\n </fs-chip>\n }\n </div>\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"entity-detail\">\n @if (unit.model) {\n <div class=\"stat\">\n <span class=\"label\">Class</span>\n <span>{{ unit.model }}</span>\n </div>\n }\n @if (unit.entity; as entity) {\n <div class=\"stat\">\n <span class=\"label\">Table</span>\n <span>{{ entity.table }}</span>\n </div>\n <!-- The handle the report AI addresses this entity by in\n describe_entity / compile_query. -->\n <div class=\"stat\">\n <span class=\"label\">Entity key</span>\n <span>{{ entity.key }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Columns</span>\n <span>{{ entity.columns }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Relationships</span>\n <span>{{ entity.relationships }}</span>\n </div>\n @if (entity.enums.length) {\n <div class=\"stat\">\n <span class=\"label\">Enumeration values</span>\n <div class=\"pair-list\">\n @for (enumColumn of entity.enums; track enumColumn.column) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ enumColumn.column }}</span>\n <span>{{ enumColumn.values.join(', ') }}</span>\n </div>\n }\n </div>\n </div>\n }\n @if (entity.computedFields.length) {\n <div class=\"stat\">\n <span class=\"label\">Computed fields</span>\n <span>{{ entity.computedFields.join(', ') }}</span>\n </div>\n }\n <div class=\"stat\">\n <span class=\"label\">Model functions</span>\n @if (entity.modelFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'model')\">\n {{ entity.modelFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n <div class=\"stat\">\n <span class=\"label\">Handler functions</span>\n @if (entity.handlerFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'handler')\">\n {{ entity.handlerFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n } @else {\n <div class=\"stat\">\n <span class=\"label\">Code functions</span>\n @if (unit.functionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'class')\">\n {{ unit.functionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n\n @if (!filteredUnits().length) {\n <div class=\"subheading\">Nothing matches \u201C{{ filter() }}\u201D.</div>\n }\n }\n\n <!-- The other half of the picture: tables the AI can't reach. Excluded\n ones have no model; unlinked junctions have a composite primary key\n but were folded into no relationship, so nothing can navigate to\n them. Both are invisible by omission \u2014 no error, just a capability\n that silently isn't there. Collapsed by default: the excluded list is\n long and mostly framework plumbing. -->\n @if (hasCatalog()) {\n <h2>Not in the semantic database</h2>\n <div class=\"subheading\">\n Tables the report AI can't see. Give one a model to promote it to an\n entity, then rebuild.\n </div>\n <mat-accordion class=\"catalog\">\n @if (excludedTables().length) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n Tables excluded ({{ excludedTables().length }})\n </mat-panel-title>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"pair-list\">\n @for (table of excludedTables(); track table.table) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ table.table }}</span>\n <span>{{ table.reason }}</span>\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n @if (unlinkedJunctions().length) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n Junctions not linked ({{ unlinkedJunctions().length }})\n </mat-panel-title>\n </mat-expansion-panel-header>\n\n <ng-template matExpansionPanelContent>\n <p class=\"small\">\n Link tables the build couldn't fold into a relationship \u2014 so\n neither they nor what they link can be reported on.\n </p>\n <div class=\"pair-list\">\n @for (table of unlinkedJunctions(); track table.table) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ table.table }}</span>\n <span>{{ table.reason }}</span>\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n }\n }\n </div>\n</div>\n", styles: [".hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}.stat{display:flex;align-items:baseline;gap:12px}.stat>.label{flex:0 0 150px;white-space:nowrap;color:#0009}.build-status{display:flex;flex-direction:column;gap:8px}.pair-list{display:flex;flex-direction:column;gap:3px}.pair-row{display:flex;gap:12px}.pair-name{flex:0 0 140px;white-space:nowrap;color:#0009}.unit-filter{width:100%;box-sizing:border-box;margin:8px 0 4px;padding:6px 10px;border:1px solid rgba(0,0,0,.2);border-radius:4px;font:inherit}.unit-filter:focus{outline:none;border-color:#00000080}.entities{display:block;padding:3px}.entities mat-panel-description{justify-content:flex-end;margin-right:8px}.entities .stage-chips{display:flex;gap:4px}.entities .entity-detail{display:flex;flex-direction:column;gap:8px}.catalog{display:block;padding:3px}.catalog .pair-name{flex-basis:220px}\n"] }]
1124
+ ], template: "<div class=\"fs-column tab-body\">\n <p class=\"hint\">\n The semantic database describes your data for reports \u2014 tables,\n relationships and the calculations in code. Reports are built from it and\n read it every time they open, so rebuild it after significant application\n updates. The build runs in the background; progress appears in the dock at\n the bottom right and this dialog can be closed while it runs.\n </p>\n\n <!-- A failed status load settles the skeleton too, so the tab says what\n happened instead of loading forever. -->\n @if (statusError()) {\n <fs-message-warning>\n The build status could not be loaded, so the last build and its coverage\n can't be shown. You can still run a build.\n </fs-message-warning>\n }\n\n <div *fsSkeleton=\"status() || statusError()\">\n @if (status(); as buildStatus) {\n <div class=\"build-status\">\n @if (buildStatus.detail.generatedAt) {\n <div class=\"stat\">\n <span class=\"label\">Last built</span>\n <fs-date\n [date]=\"buildStatus.detail.generatedAt\"\n format=\"date-time\">\n </fs-date>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Coverage</span>\n <span>{{ coverage() }}</span>\n </div>\n } @else {\n <fs-message-warning>\n The semantic database has not been built yet \u2014 reports can't be\n built or show their data until it is. Run the first build.\n </fs-message-warning>\n }\n @if (buildStatus.build) {\n <div class=\"stat\">\n <span class=\"label\">Last build run</span>\n <span>\n {{ buildStateLabel() }}\n @if (buildStatus.build.message) {\n \u2014 {{ buildStatus.build.message }}\n }\n </span>\n </div>\n }\n\n <!-- Everything above is true about what was published, and on its own it\n reads as a finished build: the structural half publishes BEFORE the\n AI function pass, so a run that died in that pass still stamps a\n date and still fills in the whole coverage line. This is the only\n line that says the build never got to the end. -->\n @if (buildStatus.detail.generatedAt && !buildComplete()) {\n <fs-message-warning>\n The last build did not finish, so this semantic database is incomplete \u2014\n {{ incompleteEntityCount() }} of {{ buildStatus.detail.entityCount }}\n entities have no code-function knowledge. Reports are built from what\n is here and cannot use the rest. Rebuild to complete it.\n </fs-message-warning>\n }\n </div>\n\n <!-- Everything the build read, in one list: entities (a table + a model +\n a handler) and standalone code classes (domain logic with no table).\n The AI reads code from both, so splitting them into two sections\n would hide half of what was built. Each row drills into its shape,\n enum values, computed fields and cached function counts \u2014 the counts\n link into the functions dialog. -->\n @if (units().length) {\n <h2>Included ({{ units().length }})</h2>\n <div class=\"subheading\">\n Everything in the semantic database. Entities can be queried; code\n classes are logic that is read and translated into SQL when a report\n is built.\n </div>\n\n <input\n class=\"unit-filter\"\n type=\"search\"\n placeholder=\"Filter by name, class or key\u2026\"\n [value]=\"filter()\"\n (input)=\"setFilter($any($event.target).value)\">\n\n <mat-accordion class=\"entities\">\n @for (unit of filteredUnits(); track unit.key) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>{{ unit.name }}</mat-panel-title>\n <mat-panel-description>\n <div class=\"stage-chips\">\n @if (!unit.hasTable) {\n <fs-chip size=\"tiny\" matTooltip=\"Domain logic with no table \u2014 read, never queried\">\n code\n </fs-chip>\n }\n <!-- Silence means built. A chip here means an interrupted\n build left this row owing content. -->\n @for (stage of pendingStages(unit); track stage) {\n <fs-chip\n size=\"tiny\"\n [backgroundColor]=\"pendingColor()\"\n color=\"#ffffff\"\n matTooltip=\"Needs building\">\n {{ stage }}\n </fs-chip>\n }\n </div>\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"entity-detail\">\n @if (unit.model) {\n <div class=\"stat\">\n <span class=\"label\">Class</span>\n <span>{{ unit.model }}</span>\n </div>\n }\n @if (unit.entity; as entity) {\n <div class=\"stat\">\n <span class=\"label\">Table</span>\n <span>{{ entity.table }}</span>\n </div>\n <!-- The handle the report AI addresses this entity by in\n describe_entity / compile_query. -->\n <div class=\"stat\">\n <span class=\"label\">Entity key</span>\n <span>{{ entity.key }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Columns</span>\n <span>{{ entity.columns }}</span>\n </div>\n <div class=\"stat\">\n <span class=\"label\">Relationships</span>\n <span>{{ entity.relationships }}</span>\n </div>\n @if (entity.enums.length) {\n <div class=\"stat\">\n <span class=\"label\">Enumeration values</span>\n <div class=\"pair-list\">\n @for (enumColumn of entity.enums; track enumColumn.column) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ enumColumn.column }}</span>\n <span>{{ enumColumn.values.join(', ') }}</span>\n </div>\n }\n </div>\n </div>\n }\n @if (entity.computedFields.length) {\n <div class=\"stat\">\n <span class=\"label\">Computed fields</span>\n <span>{{ entity.computedFields.join(', ') }}</span>\n </div>\n }\n <div class=\"stat\">\n <span class=\"label\">Model functions</span>\n @if (entity.modelFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'model')\">\n {{ entity.modelFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n <div class=\"stat\">\n <span class=\"label\">Handler functions</span>\n @if (entity.handlerFunctionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'handler')\">\n {{ entity.handlerFunctionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n } @else {\n <div class=\"stat\">\n <span class=\"label\">Code functions</span>\n @if (unit.functionCount) {\n <a\n class=\"count-link\"\n (click)=\"openFunctions(unit, 'class')\">\n {{ unit.functionCount }}\n </a>\n } @else {\n <span>0</span>\n }\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n\n @if (!filteredUnits().length) {\n <div class=\"subheading\">Nothing matches \u201C{{ filter() }}\u201D.</div>\n }\n }\n\n <!-- The other half of the picture: tables the AI can't reach. Excluded\n ones have no model; unlinked junctions have a composite primary key\n but were folded into no relationship, so nothing can navigate to\n them. Both are invisible by omission \u2014 no error, just a capability\n that silently isn't there. Collapsed by default: the excluded list is\n long and mostly framework plumbing. -->\n @if (hasCatalog()) {\n <h2>Not in the semantic database</h2>\n <div class=\"subheading\">\n Tables reports can't use. Give one a model to promote it to an\n entity, then rebuild.\n </div>\n <mat-accordion class=\"catalog\">\n @if (excludedTables().length) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n Tables excluded ({{ excludedTables().length }})\n </mat-panel-title>\n </mat-expansion-panel-header>\n\n <!-- Deferred: the body only exists once expanded. -->\n <ng-template matExpansionPanelContent>\n <div class=\"pair-list\">\n @for (table of excludedTables(); track table.table) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ table.table }}</span>\n <span>{{ table.reason }}</span>\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n @if (unlinkedJunctions().length) {\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n Junctions not linked ({{ unlinkedJunctions().length }})\n </mat-panel-title>\n </mat-expansion-panel-header>\n\n <ng-template matExpansionPanelContent>\n <p class=\"small\">\n Link tables the build couldn't fold into a relationship \u2014 so\n neither they nor what they link can be reported on.\n </p>\n <div class=\"pair-list\">\n @for (table of unlinkedJunctions(); track table.table) {\n <div class=\"pair-row\">\n <span class=\"pair-name\">{{ table.table }}</span>\n <span>{{ table.reason }}</span>\n </div>\n }\n </div>\n </ng-template>\n </mat-expansion-panel>\n }\n </mat-accordion>\n }\n }\n </div>\n</div>\n", styles: [".hint{margin:0 0 12px;color:#0009;font-size:13px;line-height:1.5}.stat{display:flex;align-items:baseline;gap:12px}.stat>.label{flex:0 0 150px;white-space:nowrap;color:#0009}.build-status{display:flex;flex-direction:column;gap:8px}.pair-list{display:flex;flex-direction:column;gap:3px}.pair-row{display:flex;gap:12px}.pair-name{flex:0 0 140px;white-space:nowrap;color:#0009}.unit-filter{width:100%;box-sizing:border-box;margin:8px 0 4px;padding:6px 10px;border:1px solid rgba(0,0,0,.2);border-radius:4px;font:inherit}.unit-filter:focus{outline:none;border-color:#00000080}.entities{display:block;padding:3px}.entities mat-panel-description{justify-content:flex-end;margin-right:8px}.entities .stage-chips{display:flex;gap:4px}.entities .entity-detail{display:flex;flex-direction:column;gap:8px}.catalog{display:block;padding:3px}.catalog .pair-name{flex-basis:220px}\n"] }]
1149
1125
  }], ctorParameters: () => [] });
1150
1126
 
1151
- // Matches the backend cap (ReportsView::INSTRUCTIONS_MAX_LENGTH) — the document
1152
- // rides along on every agent turn, so it is a prompt-budget guard.
1127
+ // Matches the backend cap (CreateReportInstructionOperation::MAX_LENGTH) — the
1128
+ // document rides along on every agent turn, so it is a prompt-budget guard.
1153
1129
  const MAX_LENGTH = 50000;
1154
1130
  // Seeded into the editor (client-side, unsaved) when no version exists yet, so
1155
1131
  // the operator sees the intended shape of the document.
@@ -1160,25 +1136,31 @@ const SCAFFOLD = [
1160
1136
  '',
1161
1137
  '# Business rules',
1162
1138
  '',
1163
- '*Rules the AI should apply when building or explaining reports.*',
1139
+ '*Rules to apply when building or explaining reports.*',
1164
1140
  '',
1165
1141
  '# Special cases',
1166
1142
  '',
1167
- '*Known exceptions and data quirks the AI cannot infer on its own.*',
1143
+ '*Known exceptions and data quirks that can\'t be worked out from the data alone.*',
1168
1144
  '',
1169
1145
  ].join('\n');
1170
- // Global report settings, opened from the reports listing's gear. A thin shell:
1171
- // the dialog frame, the tab group, and a per-tab footer. Each tab is its own
1172
- // component —
1173
- // AI Knowledge — the operator-authored markdown document appended to the
1174
- // report agent's system instructions on every chat turn. Its footer carries a
1146
+ // Global report settings, opened from the reports listing's gear. Neither tab
1147
+ // is chat-only, so nothing here is worded as AI: an install that turns the
1148
+ // report chat off and builds reports over MCP still keeps both up to date.
1149
+ // A thin shell: the dialog frame, the tab group, and a per-tab footer. Each tab
1150
+ // is its own component —
1151
+ // Knowledge — the operator-authored markdown document every report build
1152
+ // reads: the in-app agent's system instructions on each chat turn, and
1153
+ // ReadReportKnowledgeOperation for a client over MCP. Its footer carries a
1175
1154
  // View History button opening the version-history dialog (restores happen
1176
1155
  // there; the shell refreshes on close).
1177
- // Semantic Database — the build scoreboard the AI is built from, with a
1178
- // self-contained Build action.
1179
- // General — report-module settings (the semantic-build concurrency).
1180
- // The shell owns the shared data (the instructions document, the settings for
1181
- // General) plus the tab-aware save; everything else lives in the children.
1156
+ // Semantic Database — the build scoreboard reports are built and run from,
1157
+ // with a self-contained Build action.
1158
+ // There is no General tab while the build's AI function pass is disabled
1159
+ // (ReportSemanticBuildProcess::run): its one field, the semantic-build
1160
+ // concurrency, is read only by that pass. The reports/settings route that
1161
+ // stores it, and ReportData.settings()/saveSettings(), are left in place.
1162
+ // The shell owns the instructions document and the save; everything else lives
1163
+ // in the children.
1182
1164
  class GlobalSettingsComponent {
1183
1165
  selectedTab = 'knowledge';
1184
1166
  // The Semantic Database tab component, so the shared footer can drive its
@@ -1189,15 +1171,10 @@ class GlobalSettingsComponent {
1189
1171
  content = signal('');
1190
1172
  editorConfig = {
1191
1173
  maxLength: MAX_LENGTH,
1192
- placeholder: 'Terminology, business rules and special cases the report AI should know…',
1174
+ placeholder: 'Business terms, rules and special cases every report should follow…',
1193
1175
  };
1194
- // Both null until loaded — the form waits behind the skeleton until each is in.
1176
+ // null until loaded — the form waits behind the skeleton until it is in.
1195
1177
  instructions = signal(null);
1196
- settings = signal(null);
1197
- // The General tab's concurrency draft (bound to its field), seeded from settings.
1198
- concurrency = signal(0);
1199
- // Gate the whole form on both loads, so every tab's fields are ready to render.
1200
- loaded = computed(() => !!this.instructions() && !!this.settings());
1201
1178
  _dialogRef = inject(MatDialogRef);
1202
1179
  _dialog = inject(MatDialog);
1203
1180
  _reportData = inject(ReportData);
@@ -1206,23 +1183,12 @@ class GlobalSettingsComponent {
1206
1183
  constructor() {
1207
1184
  this._init();
1208
1185
  }
1209
- // The footer Save is shared, so it saves whatever the current tab owns: the
1210
- // General tab persists its settings (and stays open); AI Knowledge saves a new
1211
- // document version and closes.
1212
- save = () => {
1213
- if (this.selectedTab === 'general') {
1214
- return this._reportData.saveSettings({ semanticBuildConcurrency: this.concurrency() })
1215
- .pipe(tap((settings) => {
1216
- this._applySettings(settings);
1217
- this._message.success('Settings saved');
1218
- }));
1219
- }
1220
- return this._reportData.saveInstructions(this.content())
1221
- .pipe(tap((instructions) => {
1222
- this._message.success('AI knowledge saved');
1223
- this._dialogRef.close(instructions);
1224
- }));
1225
- };
1186
+ // Only the Knowledge tab saves: a new document version, then the dialog closes.
1187
+ save = () => this._reportData.saveInstructions(this.content())
1188
+ .pipe(tap((instructions) => {
1189
+ this._message.success('Knowledge saved');
1190
+ this._dialogRef.close(instructions);
1191
+ }));
1226
1192
  // Open the version-history dialog. Restores happen inside it, so when it
1227
1193
  // closes (however it closes) re-fetch the document: a restore changed the
1228
1194
  // current version and must reseed the editor; otherwise just refresh the
@@ -1251,20 +1217,13 @@ class GlobalSettingsComponent {
1251
1217
  this._reportData.instructions()
1252
1218
  .pipe(takeUntilDestroyed(this._destroyRef))
1253
1219
  .subscribe((instructions) => this._applyInstructions(instructions));
1254
- this._reportData.settings()
1255
- .pipe(takeUntilDestroyed(this._destroyRef))
1256
- .subscribe((settings) => this._applySettings(settings));
1257
1220
  }
1258
1221
  _applyInstructions(instructions) {
1259
1222
  this.instructions.set(instructions);
1260
1223
  this.content.set(instructions.instructions?.content ?? SCAFFOLD);
1261
1224
  }
1262
- _applySettings(settings) {
1263
- this.settings.set(settings);
1264
- this.concurrency.set(settings.semanticBuildConcurrency);
1265
- }
1266
1225
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GlobalSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1267
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: GlobalSettingsComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "semanticDatabase", first: true, predicate: SemanticDatabaseComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<form\n *fsSkeletonForm=\"loaded()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n <mat-tab\n label=\"General\"\n name=\"general\">\n <fs-ai-report-global-settings-general\n [concurrency]=\"concurrency()\"\n (concurrencyChange)=\"concurrency.set($event)\"\n [min]=\"settings()!.semanticBuildConcurrencyMin\"\n [max]=\"settings()!.semanticBuildConcurrencyMax\">\n </fs-ai-report-global-settings-general>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); General uses the default Save/Cancel; the\n Semantic Database tab is a read/run surface, so it gets a primary\n Close plus its Build/Rebuild/Stop action. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'general'\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n <!-- Never gated on the status load: a status response that fails\n must not take the Build action away with it. -->\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ db.rebuild() ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i2$1.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$2.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "component", type: i2$2.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$2.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonFormDirective, selector: "[fsSkeletonForm]", inputs: ["fsSkeletonForm", "fsSkeletonFormLines"] }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i6.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i6.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: AiKnowledgeComponent, selector: "fs-ai-report-global-settings-ai-knowledge", inputs: ["content", "config"], outputs: ["contentChange"] }, { kind: "component", type: GeneralComponent, selector: "fs-ai-report-global-settings-general", inputs: ["concurrency", "min", "max"], outputs: ["concurrencyChange"] }, { kind: "component", type: SemanticDatabaseComponent, selector: "fs-ai-report-global-settings-semantic-database" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1226
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: GlobalSettingsComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "semanticDatabase", first: true, predicate: SemanticDatabaseComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<form\n *fsSkeletonForm=\"instructions()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); the Semantic Database tab is a read/run\n surface, so it gets a primary Close plus its Build/Rebuild/Stop\n action. Neither uses the default Save/Cancel pair. -->\n <fs-form-dialog-actions [save]=\"false\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n <!-- Never gated on the status load: a status response that fails\n must not take the Build action away with it. -->\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ db.rebuild() ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i2$1.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i3.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "component", type: i3.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i3.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonFormDirective, selector: "[fsSkeletonForm]", inputs: ["fsSkeletonForm", "fsSkeletonFormLines"] }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i7.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i7.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i6.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i6.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: AiKnowledgeComponent, selector: "fs-ai-report-global-settings-ai-knowledge", inputs: ["content", "config"], outputs: ["contentChange"] }, { kind: "component", type: SemanticDatabaseComponent, selector: "fs-ai-report-global-settings-semantic-database" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1268
1227
  }
1269
1228
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GlobalSettingsComponent, decorators: [{
1270
1229
  type: Component,
@@ -1280,9 +1239,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
1280
1239
  MatDialogContent,
1281
1240
  MatTabsModule,
1282
1241
  AiKnowledgeComponent,
1283
- GeneralComponent,
1284
1242
  SemanticDatabaseComponent,
1285
- ], template: "<form\n *fsSkeletonForm=\"loaded()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n <mat-tab\n label=\"General\"\n name=\"general\">\n <fs-ai-report-global-settings-general\n [concurrency]=\"concurrency()\"\n (concurrencyChange)=\"concurrency.set($event)\"\n [min]=\"settings()!.semanticBuildConcurrencyMin\"\n [max]=\"settings()!.semanticBuildConcurrencyMax\">\n </fs-ai-report-global-settings-general>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); General uses the default Save/Cancel; the\n Semantic Database tab is a read/run surface, so it gets a primary\n Close plus its Build/Rebuild/Stop action. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'general'\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n <!-- Never gated on the status load: a status response that fails\n must not take the Build action away with it. -->\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ db.rebuild() ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"] }]
1243
+ ], template: "<form\n *fsSkeletonForm=\"instructions()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); the Semantic Database tab is a read/run\n surface, so it gets a primary Close plus its Build/Rebuild/Stop\n action. Neither uses the default Save/Cancel pair. -->\n <fs-form-dialog-actions [save]=\"false\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n <!-- Never gated on the status load: a status response that fails\n must not take the Build action away with it. -->\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ db.rebuild() ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"] }]
1286
1244
  }], ctorParameters: () => [] });
1287
1245
 
1288
1246
  // What a failed request says: the server's message, else the error's own,
@@ -1393,7 +1351,7 @@ class ReportPackageImportComponent {
1393
1351
  return { context: new HttpContext().set(DisplayApiError, false) };
1394
1352
  }
1395
1353
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportPackageImportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1396
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportPackageImportComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<fs-dialog>\n <fs-dialog-title>\n Import Report\n <ng-template fsDialogSubtitle>\n {{ fileName() }}\n </ng-template>\n </fs-dialog-title>\n <mat-dialog-content>\n <!-- The check runs every query in the package, so it can take a while:\n the skeleton holds until it answers or fails. -->\n <ng-container *fsSkeleton=\"check() || checkError()\">\n @if (checkError()) {\n <fs-message-error>\n {{ checkError() }}\n </fs-message-error>\n }\n\n @if (check(); as packageCheck) {\n <div class=\"fs-column.gap-md\">\n @if (!packageCheck.valid) {\n <fs-message-error>\n Can't be imported. Every error below has to be fixed first.\n </fs-message-error>\n } @else if (hasWarnings()) {\n <fs-message-warning>\n Ready with warnings. Review the warnings below before importing.\n </fs-message-warning>\n } @else {\n <fs-message-success>\n Ready to import.\n </fs-message-success>\n }\n\n <!-- Nothing to describe when the archive could not be read: its one\n error finding says why. -->\n @if (packageCheck.name) {\n <div class=\"fs-column\">\n <fs-label-field>\n <fs-label>Report</fs-label>\n {{ packageCheck.name }}\n </fs-label-field>\n <fs-label-field>\n <fs-label>Imports into</fs-label>\n {{ destination() }}\n </fs-label-field>\n @if (packageCheck.source; as source) {\n <fs-label-field>\n <fs-label>Exported from</fs-label>\n {{ source.environment }}\n <div class=\"small\">\n {{ source.url }}\n </div>\n </fs-label-field>\n <fs-label-field>\n <fs-label>Exported</fs-label>\n {{ source.exportedAt | fsDate: 'date-time' }}\n @if (source.exportedBy) {\n by {{ source.exportedBy }}\n }\n </fs-label-field>\n }\n @if (contents()) {\n <fs-label-field>\n <fs-label>Contents</fs-label>\n {{ contents() }}\n </fs-label-field>\n }\n </div>\n }\n\n @for (section of sections(); track section.severity) {\n <div>\n <h3>\n {{ section.heading }} ({{ section.findings.length }})\n </h3>\n <div class=\"fs-column.gap-sm\">\n @for (finding of section.findings; track $index) {\n <div>\n @if (finding.location) {\n <div class=\"small\">\n {{ finding.location }}\n </div>\n }\n {{ finding.message }}\n </div>\n }\n </div>\n </div>\n }\n\n @if (packageCheck.valid && hasWarnings()) {\n <mat-checkbox\n [checked]=\"warningsReviewed()\"\n [disabled]=\"busy()\"\n (change)=\"warningsReviewed.set($event.checked)\">\n I've reviewed the warnings\n </mat-checkbox>\n }\n\n @if (importError()) {\n <fs-message-error>\n {{ importError() }}\n </fs-message-error>\n }\n </div>\n }\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions [save]=\"false\">\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [disabled]=\"busy() || !canImport()\"\n (click)=\"importPackage()\">\n @if (busy()) {\n <mat-spinner [diameter]=\"20\"></mat-spinner>\n } @else {\n <span>Import</span>\n }\n </button>\n <button\n type=\"button\"\n mat-button\n [disabled]=\"busy()\"\n [mat-dialog-close]=\"undefined\">\n Cancel\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n</fs-dialog>\n", dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "pipe", type: i2.FsDatePipe, name: "fsDate" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i2$1.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "directive", type: i2$1.FsDialogSubtitleDirective, selector: "[fsDialogSubtitle]" }, { kind: "component", type: FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "component", type: i5$1.FsLabelComponent, selector: "fs-label" }, { kind: "component", type: i5$1.FsLabelFieldComponent, selector: "fs-label-field", inputs: ["appearance", "showOutline", "disabled", "focused", "hoverable", "padless"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageWarningComponent, selector: "fs-message-warning" }, { kind: "component", type: i4.FsMessageSuccessComponent, selector: "fs-message-success" }, { kind: "component", type: i4.FsMessageErrorComponent, selector: "fs-message-error" }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1354
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportPackageImportComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<fs-dialog>\n <fs-dialog-title>\n Import Report\n <ng-template fsDialogSubtitle>\n {{ fileName() }}\n </ng-template>\n </fs-dialog-title>\n <mat-dialog-content>\n <!-- The check runs every query in the package, so it can take a while:\n the skeleton holds until it answers or fails. -->\n <ng-container *fsSkeleton=\"check() || checkError()\">\n @if (checkError()) {\n <fs-message-error>\n {{ checkError() }}\n </fs-message-error>\n }\n\n @if (check(); as packageCheck) {\n <div class=\"fs-column.gap-md\">\n @if (!packageCheck.valid) {\n <fs-message-error>\n Can't be imported. Every error below has to be fixed first.\n </fs-message-error>\n } @else if (hasWarnings()) {\n <fs-message-warning>\n Ready with warnings. Review the warnings below before importing.\n </fs-message-warning>\n } @else {\n <fs-message-success>\n Ready to import.\n </fs-message-success>\n }\n\n <!-- Nothing to describe when the archive could not be read: its one\n error finding says why. -->\n @if (packageCheck.name) {\n <div class=\"fs-column\">\n <fs-label-field>\n <fs-label>Report</fs-label>\n {{ packageCheck.name }}\n </fs-label-field>\n <fs-label-field>\n <fs-label>Imports into</fs-label>\n {{ destination() }}\n </fs-label-field>\n @if (packageCheck.source; as source) {\n <fs-label-field>\n <fs-label>Exported from</fs-label>\n {{ source.environment }}\n <div class=\"small\">\n {{ source.url }}\n </div>\n </fs-label-field>\n <fs-label-field>\n <fs-label>Exported</fs-label>\n {{ source.exportedAt | fsDate: 'date-time' }}\n @if (source.exportedBy) {\n by {{ source.exportedBy }}\n }\n </fs-label-field>\n }\n @if (contents()) {\n <fs-label-field>\n <fs-label>Contents</fs-label>\n {{ contents() }}\n </fs-label-field>\n }\n </div>\n }\n\n @for (section of sections(); track section.severity) {\n <div>\n <h3>\n {{ section.heading }} ({{ section.findings.length }})\n </h3>\n <div class=\"fs-column.gap-sm\">\n @for (finding of section.findings; track $index) {\n <div>\n @if (finding.location) {\n <div class=\"small\">\n {{ finding.location }}\n </div>\n }\n {{ finding.message }}\n </div>\n }\n </div>\n </div>\n }\n\n @if (packageCheck.valid && hasWarnings()) {\n <mat-checkbox\n [checked]=\"warningsReviewed()\"\n [disabled]=\"busy()\"\n (change)=\"warningsReviewed.set($event.checked)\">\n I've reviewed the warnings\n </mat-checkbox>\n }\n\n @if (importError()) {\n <fs-message-error>\n {{ importError() }}\n </fs-message-error>\n }\n </div>\n }\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions [save]=\"false\">\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [disabled]=\"busy() || !canImport()\"\n (click)=\"importPackage()\">\n @if (busy()) {\n <mat-spinner [diameter]=\"20\"></mat-spinner>\n } @else {\n <span>Import</span>\n }\n </button>\n <button\n type=\"button\"\n mat-button\n [disabled]=\"busy()\"\n [mat-dialog-close]=\"undefined\">\n Cancel\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n</fs-dialog>\n", dependencies: [{ kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "ngmodule", type: FsDateModule }, { kind: "pipe", type: i2.FsDatePipe, name: "fsDate" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i2$1.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { kind: "directive", type: i2$1.FsDialogSubtitleDirective, selector: "[fsDialogSubtitle]" }, { kind: "component", type: FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "component", type: i6$1.FsLabelComponent, selector: "fs-label" }, { kind: "component", type: i6$1.FsLabelFieldComponent, selector: "fs-label-field", inputs: ["appearance", "showOutline", "disabled", "focused", "hoverable", "padless"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageWarningComponent, selector: "fs-message-warning" }, { kind: "component", type: i4.FsMessageSuccessComponent, selector: "fs-message-success" }, { kind: "component", type: i4.FsMessageErrorComponent, selector: "fs-message-error" }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1397
1355
  }
1398
1356
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportPackageImportComponent, decorators: [{
1399
1357
  type: Component,
@@ -1983,7 +1941,7 @@ class FsAiReportsComponent {
1983
1941
  return segments;
1984
1942
  }
1985
1943
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1986
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "_tree", first: true, predicate: FsTreeComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div\n class=\"fs-row.gap-sm.align-center node-row\"\n [class.deleted]=\"data.deleted\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder || data.deleted) {\n <!-- A deleted report can't be opened (the backend only assembles\n live ones, and one under a deleted folder is not on the live\n listing), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n\n <!-- Only on the row somebody deleted. Everything under a deleted\n folder is muted with it, but was not deleted itself. -->\n @if (data.stateDeleted) {\n <fs-chip size=\"tiny\">Deleted</fs-chip>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-row.deleted{opacity:.55}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"], dependencies: [{ kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$3.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i4$2.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i4$2.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1944
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "_tree", first: true, predicate: FsTreeComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div\n class=\"fs-row.gap-sm.align-center node-row\"\n [class.deleted]=\"data.deleted\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder || data.deleted) {\n <!-- A deleted report can't be opened (the backend only assembles\n live ones, and one under a deleted folder is not on the live\n listing), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n\n <!-- Only on the row somebody deleted. Everything under a deleted\n folder is muted with it, but was not deleted itself. -->\n @if (data.stateDeleted) {\n <fs-chip size=\"tiny\">Deleted</fs-chip>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-row.deleted{opacity:.55}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"], dependencies: [{ kind: "ngmodule", type: FsChipModule }, { kind: "component", type: i1.FsChipComponent, selector: "fs-chip", inputs: ["selectable", "removable", "value", "maxWidth", "width", "backgroundColor", "borderColor", "color", "shape", "outlined", "outlineDash", "icon", "image", "selected", "disabled", "padding", "contrastColor", "size"], outputs: ["selectedToggled", "removed", "click"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$3.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i4$2.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i4$2.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1987
1945
  }
1988
1946
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
1989
1947
  type: Component,
@@ -2025,7 +1983,9 @@ function canvasLandingPage(input) {
2025
1983
 
2026
1984
  // How a time axis writes its period labels: short ('May', with the year on a
2027
1985
  // second line when the data spans years), long ('May 2026', in the report's
2028
- // locale) or numeric ('2026-05', the same in every locale). A category axis
1986
+ // locale), numeric ('2026-05', the same in every locale) or periodEnd (the
1987
+ // period's last day as a numeric date in the locale: a week its Sunday
1988
+ // '9/7/2025', a month '9/30/2025', a year '12/31/2025'). A category axis
2029
1989
  // prints its values as they are.
2030
1990
  //
2031
1991
  // Mirrors the server's ChartDateLabelEnum value for value.
@@ -2034,8 +1994,16 @@ var ReportChartDateLabel;
2034
1994
  ReportChartDateLabel["Short"] = "short";
2035
1995
  ReportChartDateLabel["Long"] = "long";
2036
1996
  ReportChartDateLabel["Numeric"] = "numeric";
1997
+ ReportChartDateLabel["PeriodEnd"] = "periodEnd";
2037
1998
  })(ReportChartDateLabel || (ReportChartDateLabel = {}));
2038
1999
 
2000
+ // Report weeks start on Monday. This is a rule, not a setting: the server's
2001
+ // weekly time buckets (ReportTimeBucket::getPeriodStart() at the Week grain)
2002
+ // and its relative date presets (ReportRelativeRangeEnum) both start the week
2003
+ // on Monday, so the viewer's thisWeek/lastWeek presets must too, or a weekly
2004
+ // chart's buckets would straddle the range its own filter names.
2005
+ const REPORT_WEEK_STARTS_ON = 1;
2006
+
2039
2007
  // A chart measure's named color. The names follow the report chart palette in
2040
2008
  // order (REPORT_CHART_COLOR_BY_NAME), so blue is the first series' default
2041
2009
  // color and gray the tenth's.
@@ -2793,6 +2761,8 @@ const MONTH_LONG = { month: 'long' };
2793
2761
  const MONTH_YEAR = { year: 'numeric', month: 'long' };
2794
2762
  // 'Jan 5' / '5 janv.': a daily or weekly period label.
2795
2763
  const MONTH_DAY = { month: 'short', day: 'numeric' };
2764
+ // '9/7/2025' / '2025-09-07': a period-end label, the period's last day.
2765
+ const DAY_NUMERIC = { year: 'numeric', month: 'numeric', day: 'numeric' };
2796
2766
  // A date in the locale. With a zone, the day it falls on there; without one,
2797
2767
  // the browser's zone, which keeps a calendar day parsed as local midnight
2798
2768
  // unshifted. It never throws, since a pipe or an export calls it per cell: an
@@ -5643,23 +5613,46 @@ function chartDateLabel(config) {
5643
5613
  ?? ReportChartDateLabel.Short;
5644
5614
  }
5645
5615
  // A bucket's period-start split into its primary label and its year. A year
5646
- // reads "2025" in every style. Short, in the locale: day/week read "May 1"
5647
- // ('1 mai' in fr-CA), month "May" ('mai'), quarter "Q2" ('T2' in French).
5648
- // Long carries its year, in the locale: "May 1, 2026", "May 2026" ('mai
5649
- // 2026'), "Q2 2026". Numeric is the same in every locale: "2026-05-01",
5650
- // "2026-05", "2026-Q2". Returns null for a value that isn't a parseable date
5651
- // (left as-is by callers).
5616
+ // reads "2025" in every style but periodEnd. Short, in the locale: day/week
5617
+ // read "May 1" ('1 mai' in fr-CA), month "May" ('mai'), quarter "Q2" ('T2' in
5618
+ // French). Long carries its year, in the locale: "May 1, 2026", "May 2026"
5619
+ // ('mai 2026'), "Q2 2026". Numeric is the same in every locale: "2026-05-01",
5620
+ // "2026-05", "2026-Q2". PeriodEnd is the period's last day as a numeric date
5621
+ // in the locale, at every grain: a week its Sunday "9/7/2025" ('2025-09-07'
5622
+ // in fr-CA), a month "9/30/2025", a quarter "9/30/2025", a year "12/31/2025",
5623
+ // a day itself. Returns null for a value that isn't a parseable date (left
5624
+ // as-is by callers).
5652
5625
  function periodLabelParts(value, granularity, locale, dateLabel = ReportChartDateLabel.Short) {
5653
5626
  const date = parseISO(value);
5654
5627
  if (!isValid(date)) {
5655
5628
  return null;
5656
5629
  }
5630
+ if (dateLabel === ReportChartDateLabel.PeriodEnd) {
5631
+ const end = periodEndDay(date, granularity);
5632
+ return { primary: formatLocalDate(end, locale, DAY_NUMERIC), year: String(end.getFullYear()) };
5633
+ }
5657
5634
  const year = String(date.getFullYear());
5658
5635
  if (granularity === 'year') {
5659
5636
  return { primary: year, year };
5660
5637
  }
5661
5638
  return { primary: periodPrimaryLabel(date, granularity, locale, dateLabel), year };
5662
5639
  }
5640
+ // The last day of the period that starts on `start`, as the server's
5641
+ // period-end cells write it: a week ends on the Sunday after its Monday.
5642
+ function periodEndDay(start, granularity) {
5643
+ switch (granularity) {
5644
+ case 'week':
5645
+ return startOfDay(endOfWeek(start, { weekStartsOn: REPORT_WEEK_STARTS_ON }));
5646
+ case 'month':
5647
+ return startOfDay(endOfMonth(start));
5648
+ case 'quarter':
5649
+ return startOfDay(endOfQuarter(start));
5650
+ case 'year':
5651
+ return startOfDay(endOfYear(start));
5652
+ default: // day — the day itself
5653
+ return start;
5654
+ }
5655
+ }
5663
5656
  // A period's label in the style asked for, the year aside (see
5664
5657
  // periodLabelParts).
5665
5658
  function periodPrimaryLabel(date, granularity, locale, dateLabel) {
@@ -5690,7 +5683,7 @@ function periodPrimaryLabel(date, granularity, locale, dateLabel) {
5690
5683
  }
5691
5684
  }
5692
5685
  // Whether a period label leaves its year to be added apart: only a short one
5693
- // does, a long or numeric label already carries it.
5686
+ // does, a long, numeric or period-end label already carries it.
5694
5687
  function periodYearApart(granularity, dateLabel) {
5695
5688
  return granularity !== 'year' && dateLabel === ReportChartDateLabel.Short;
5696
5689
  }
@@ -7306,13 +7299,6 @@ function operandSpec(formula, layerValues) {
7306
7299
  return byId(layerValues).filter((value) => valueKey(value) === first).pop()?.config?.measure;
7307
7300
  }
7308
7301
 
7309
- // Report weeks start on Monday. This is a rule, not a setting: the server's
7310
- // weekly time buckets (ReportTimeBucket::getPeriodStart() at the Week grain)
7311
- // and its relative date presets (ReportRelativeRangeEnum) both start the week
7312
- // on Monday, so the viewer's thisWeek/lastWeek presets must too, or a weekly
7313
- // chart's buckets would straddle the range its own filter names.
7314
- const REPORT_WEEK_STARTS_ON = 1;
7315
-
7316
7302
  // The most leading columns a list can keep in view while it scrolls sideways.
7317
7303
  // Mirrors the server's ListConfig::MAX_PINNED_COLUMNS, which refuses more.
7318
7304
  const MAX_PINNED_COLUMNS = 3;
@@ -9004,6 +8990,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
9004
8990
  ], template: "<fs-dialog>\n <fs-dialog-title>\n Columns\n </fs-dialog-title>\n <mat-dialog-content>\n <div class=\"fs-column.gap-xs\">\n @for (column of columns; track column.column) {\n <mat-checkbox\n class=\"column-option\"\n [checked]=\"!hidden().has(column.column)\"\n [disabled]=\"isLastShown(column.column)\"\n (change)=\"toggle(column.column, $event.checked)\">\n {{ column.label }}\n </mat-checkbox>\n }\n </div>\n </mat-dialog-content>\n <mat-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n (click)=\"done()\">\n Done\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"undefined\">\n Cancel\n </button>\n </mat-dialog-actions>\n</fs-dialog>\n", styles: [".column-option{overflow-wrap:anywhere}\n"] }]
9005
8991
  }] });
9006
8992
 
8993
+ // A time-axis chart's settings-dialog keys (dateLabel and span), from its
8994
+ // config.xAxis; null where the config has none.
8995
+ function chartAxisSettings(config) {
8996
+ return {
8997
+ dateLabel: config?.xAxis?.dateLabel ?? null,
8998
+ span: config?.xAxis?.span ?? null,
8999
+ };
9000
+ }
9001
+ // Whether two sets of time-axis keys draw the same chart. Short and an absent
9002
+ // dateLabel are one, as are a span's absent and null periods or start.
9003
+ function sameChartAxisSettings(a, b) {
9004
+ return JSON.stringify(normalizedAxisSettings(a)) === JSON.stringify(normalizedAxisSettings(b));
9005
+ }
9006
+ function normalizedAxisSettings(settings) {
9007
+ const { dateLabel, span } = settings;
9008
+ return {
9009
+ dateLabel: dateLabel === ReportChartDateLabel.Short ? null : (dateLabel ?? null),
9010
+ span: span
9011
+ ? { through: span.through, periods: span.periods ?? undefined, start: span.start || undefined }
9012
+ : null,
9013
+ };
9014
+ }
9015
+
9007
9016
  // Which way a KPI's change points, as shown: up, down, or flat when the
9008
9017
  // change rounds to zero at the digits the card prints. A flat change draws no
9009
9018
  // arrow.
@@ -12075,14 +12084,25 @@ class ReportComponentComponent {
12075
12084
  // (ngx-echarts observes the host), so nothing is told to resize. A KPI's card
12076
12085
  // keys are different: settings.kpi carries the keys the server saved (never
12077
12086
  // the ones the dialog sent), and the server sends the target's progress and
12078
- // the sparkline's points with the data, so a change to them reloads it.
12087
+ // the sparkline's points with the data, so a change to them reloads it. So
12088
+ // are a chart's time-axis keys (settings.chartAxis): the server runs a
12089
+ // span, and the chart builds its option from the data it reloads, which
12090
+ // draws a new date label too.
12079
12091
  applySettings(settings) {
12092
+ const config = this.component.config;
12080
12093
  const cardChanged = !!settings.kpi
12081
12094
  && !sameKpiCardSettings(settings.kpi, kpiCardSettings(this.component.config));
12095
+ const axisChanged = !!settings.chartAxis
12096
+ && !sameChartAxisSettings(settings.chartAxis, chartAxisSettings(config));
12082
12097
  this.component.title = settings.title;
12083
- this.component.config = { ...this.component.config, padding: settings.padding, ...(settings.kpi ?? {}) };
12098
+ this.component.config = {
12099
+ ...this.component.config,
12100
+ padding: settings.padding,
12101
+ ...(settings.kpi ?? {}),
12102
+ ...(settings.chartAxis ? { xAxis: this._axisWith(config, settings) } : {}),
12103
+ };
12084
12104
  this._buildFilterConfig();
12085
- if (cardChanged) {
12105
+ if (cardChanged || axisChanged) {
12086
12106
  this._refresh$.next();
12087
12107
  }
12088
12108
  }
@@ -12103,6 +12123,17 @@ class ReportComponentComponent {
12103
12123
  exportExcel() {
12104
12124
  this._export('xlsx');
12105
12125
  }
12126
+ // The chart's xAxis with the time-axis keys the server saved: a key it
12127
+ // removed is left out, every other xAxis key kept.
12128
+ _axisWith(config, settings) {
12129
+ const { dateLabel, span, ...axis } = config.xAxis ?? { column: '' };
12130
+ const saved = settings.chartAxis;
12131
+ return {
12132
+ ...axis,
12133
+ ...(saved?.dateLabel ? { dateLabel: saved.dateLabel } : {}),
12134
+ ...(saved?.span ? { span: saved.span } : {}),
12135
+ };
12136
+ }
12106
12137
  _export(format) {
12107
12138
  const filters = this._filterState.resolveForComponent(this.component);
12108
12139
  const layout = this.component.type === 'grid' ? this._filterState.gridSessionLayout(this.component) : null;
@@ -12465,7 +12496,7 @@ class ReportComponentComponent {
12465
12496
  openAtPoint(anchor, click.pointer, this.zoom, () => this.markMenu?.openMenu(null));
12466
12497
  }
12467
12498
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
12468
- 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$4.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$4.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$4.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 });
12499
+ 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 });
12469
12500
  }
12470
12501
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponentComponent, decorators: [{
12471
12502
  type: Component,
@@ -13400,7 +13431,7 @@ class ReportCanvasComponent {
13400
13431
  this._guides?.nativeElement?.replaceChildren();
13401
13432
  }
13402
13433
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13403
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportCanvasComponent, isStandalone: true, selector: "app-report-canvas", inputs: { report: "report", editMode: "editMode", pageId: "pageId" }, outputs: { componentSettings: "componentSettings", reportChanged: "reportChanged", pageChanged: "pageChanged" }, viewQueries: [{ propertyName: "zoomPan", first: true, predicate: FsZoomPanComponent, descendants: true }, { propertyName: "_viewport", first: true, predicate: ["viewport"], descendants: true, static: true }, { propertyName: "_guides", first: true, predicate: ["guides"], descendants: true }, { propertyName: "_renderedComponents", predicate: ReportComponentComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. While the\n header and footer are edited, an empty band says how to fill it\n and a double-click adds a text block there. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.headerHeight * PX\"\n (dblclick)=\"addTextInBand('header', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n @if (isBandEmpty('header')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.footerHeight * PX\"\n (dblclick)=\"addTextInBand('footer', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n @if (isBandEmpty('footer')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\n <div class=\"page-empty\">\n Ask the assistant to add a chart, list or KPI to this page.\n </div>\n }\n </div>\n }\n </fs-zoom-pan>\n </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\n }\n <button\n type=\"button\"\n class=\"page-tab add-page\"\n matTooltip=\"Add page\"\n (click)=\"addPage()\">\n <mat-icon>add</mat-icon>\n Page\n </button>\n </div>\n\n <div class=\"zoom-controls\">\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom out (Ctrl/Cmd + scroll)\"\n (click)=\"zoomOut()\">\n <mat-icon>remove</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-label\"\n matTooltip=\"Zoom to 100%\"\n (click)=\"zoomActual()\">\n {{ (zoom() * 100).toFixed(0) }}%\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom in (Ctrl/Cmd + scroll)\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-width:0;min-height:0;flex:1;margin-top:10px}.canvas{display:flex;flex-direction:column;gap:8px;flex:1;min-height:0}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px}.canvas-toolbar .page-tabs{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;cursor:pointer}.canvas-toolbar .page-tabs .page-tab.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.canvas-toolbar .page-tabs .page-tab .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 8px;border-style:dashed;color:#52606d}.canvas-toolbar .page-tabs .page-tab.add-page mat-icon{font-size:16px;width:16px;height:16px}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.canvas-toolbar .zoom-controls{display:flex;align-items:center;gap:2px;margin-left:auto;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .zoom-controls .zoom-button{display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-button:hover{background:#f0f4f8}.canvas-toolbar .zoom-controls .zoom-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .zoom-controls .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-label:hover{color:#1f2933}.page-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.page-viewport{position:relative;flex:1;min-height:420px;border:1px solid #e4e7eb;border-radius:8px;background:#eef1f5;overflow:hidden}.page-viewport fs-zoom-pan{width:100%;height:100%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band.addable{pointer-events:auto;cursor:text;-webkit-user-select:none;user-select:none}.band .band-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));color:#9aa5b1;white-space:nowrap}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.guides{position:absolute;inset:0;pointer-events:none;z-index:30}.guides ::ng-deep .guide{position:absolute;background:#2196f3}.guides ::ng-deep .guide.guide-v{top:0;bottom:0;width:calc(1.25px / var(--canvas-zoom, 1))}.guides ::ng-deep .guide.guide-h{left:0;right:0;height:calc(1.25px / var(--canvas-zoom, 1))}.page-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;font-size:14px;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$5.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$5.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$4.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$4.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$4.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "ngmodule", type: FsZoomPanModule }, { kind: "component", type: i3.FsZoomPanComponent, selector: "fs-zoom-pan", inputs: ["zoomMax", "zoomMin", "zoomScale", "zoomDefault", "zoomFactor", "zoomFit", "fitMode", "fitPosition", "fitPadding", "fitMaxScale", "wheel", "top", "left"], outputs: ["moved", "zoomed"] }, { kind: "component", type: ReportComponentComponent, selector: "app-report-component", inputs: ["reportId", "component", "groups", "editMode", "zoom", "snapper", "selected", "tokens", "valueKeysElsewhere"], outputs: ["selectComponent", "positionChanged", "openSettings", "removed", "valueSettings", "textSaved", "valuesRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13434
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportCanvasComponent, isStandalone: true, selector: "app-report-canvas", inputs: { report: "report", editMode: "editMode", pageId: "pageId" }, outputs: { componentSettings: "componentSettings", reportChanged: "reportChanged", pageChanged: "pageChanged" }, viewQueries: [{ propertyName: "zoomPan", first: true, predicate: FsZoomPanComponent, descendants: true }, { propertyName: "_viewport", first: true, predicate: ["viewport"], descendants: true, static: true }, { propertyName: "_guides", first: true, predicate: ["guides"], descendants: true }, { propertyName: "_renderedComponents", predicate: ReportComponentComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. While the\n header and footer are edited, an empty band says how to fill it\n and a double-click adds a text block there. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.headerHeight * PX\"\n (dblclick)=\"addTextInBand('header', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n @if (isBandEmpty('header')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [class.addable]=\"isMasterEditing\"\n [style.height.px]=\"bands.footerHeight * PX\"\n (dblclick)=\"addTextInBand('footer', $event)\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n @if (isBandEmpty('footer')) {\n <span class=\"band-hint small\">Double-click to add text</span>\n }\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\n <div class=\"page-empty\">\n Ask the assistant to add a chart, list or KPI to this page.\n </div>\n }\n </div>\n }\n </fs-zoom-pan>\n </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\n }\n <button\n type=\"button\"\n class=\"page-tab add-page\"\n matTooltip=\"Add page\"\n (click)=\"addPage()\">\n <mat-icon>add</mat-icon>\n Page\n </button>\n </div>\n\n <div class=\"zoom-controls\">\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom out (Ctrl/Cmd + scroll)\"\n (click)=\"zoomOut()\">\n <mat-icon>remove</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-label\"\n matTooltip=\"Zoom to 100%\"\n (click)=\"zoomActual()\">\n {{ (zoom() * 100).toFixed(0) }}%\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom in (Ctrl/Cmd + scroll)\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-width:0;min-height:0;flex:1;margin-top:10px}.canvas{display:flex;flex-direction:column;gap:8px;flex:1;min-height:0}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px}.canvas-toolbar .page-tabs{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;cursor:pointer}.canvas-toolbar .page-tabs .page-tab.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.canvas-toolbar .page-tabs .page-tab .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 8px;border-style:dashed;color:#52606d}.canvas-toolbar .page-tabs .page-tab.add-page mat-icon{font-size:16px;width:16px;height:16px}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.canvas-toolbar .zoom-controls{display:flex;align-items:center;gap:2px;margin-left:auto;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .zoom-controls .zoom-button{display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-button:hover{background:#f0f4f8}.canvas-toolbar .zoom-controls .zoom-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .zoom-controls .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-label:hover{color:#1f2933}.page-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.page-viewport{position:relative;flex:1;min-height:420px;border:1px solid #e4e7eb;border-radius:8px;background:#eef1f5;overflow:hidden}.page-viewport fs-zoom-pan{width:100%;height:100%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band.addable{pointer-events:auto;cursor:text;-webkit-user-select:none;user-select:none}.band .band-hint{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));color:#9aa5b1;white-space:nowrap}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.guides{position:absolute;inset:0;pointer-events:none;z-index:30}.guides ::ng-deep .guide{position:absolute;background:#2196f3}.guides ::ng-deep .guide.guide-v{top:0;bottom:0;width:calc(1.25px / var(--canvas-zoom, 1))}.guides ::ng-deep .guide.guide-h{left:0;right:0;height:calc(1.25px / var(--canvas-zoom, 1))}.page-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;font-size:14px;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$5.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$5.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "ngmodule", type: FsZoomPanModule }, { kind: "component", type: i3$1.FsZoomPanComponent, selector: "fs-zoom-pan", inputs: ["zoomMax", "zoomMin", "zoomScale", "zoomDefault", "zoomFactor", "zoomFit", "fitMode", "fitPosition", "fitPadding", "fitMaxScale", "wheel", "top", "left"], outputs: ["moved", "zoomed"] }, { kind: "component", type: ReportComponentComponent, selector: "app-report-component", inputs: ["reportId", "component", "groups", "editMode", "zoom", "snapper", "selected", "tokens", "valueKeysElsewhere"], outputs: ["selectComponent", "positionChanged", "openSettings", "removed", "valueSettings", "textSaved", "valuesRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13404
13435
  }
13405
13436
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportCanvasComponent, decorators: [{
13406
13437
  type: Component,
@@ -13460,6 +13491,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
13460
13491
  type: Injectable
13461
13492
  }] });
13462
13493
 
13494
+ // What a chart's settings dialog offers for the periods its time axis draws:
13495
+ // the viewer's date range as it is (none, no span), the last N periods, or
13496
+ // every period since a fixed day, each up to the end of the date filter.
13497
+ var ReportChartSpanMode;
13498
+ (function (ReportChartSpanMode) {
13499
+ ReportChartSpanMode["None"] = "none";
13500
+ ReportChartSpanMode["Periods"] = "periods";
13501
+ ReportChartSpanMode["Start"] = "start";
13502
+ })(ReportChartSpanMode || (ReportChartSpanMode = {}));
13503
+
13504
+ // Where a chart's xAxis.span ends: at the end of the date filter the viewer
13505
+ // set, else the end of the document's own timeframe on the axis, else the
13506
+ // current period. The server lays the span over the axis and runs it, so the
13507
+ // screen and every export draw the same periods.
13508
+ //
13509
+ // Mirrors the server's ChartSpanThroughEnum value for value.
13510
+ var ReportChartSpanThrough;
13511
+ (function (ReportChartSpanThrough) {
13512
+ ReportChartSpanThrough["FilterEnd"] = "filterEnd";
13513
+ })(ReportChartSpanThrough || (ReportChartSpanThrough = {}));
13514
+
13463
13515
  // The Type select's choices. A column that can be either holds both.
13464
13516
  const FILTER_TYPE_OPTIONS = [
13465
13517
  { value: 'select', name: 'Values' },
@@ -13757,6 +13809,23 @@ const KPI_CHANGE_POLARITY_OPTIONS = [
13757
13809
  // A KPI sparkline's periods stay within 2 to 60.
13758
13810
  const KPI_SPARKLINE_MIN_PERIODS = 2;
13759
13811
  const KPI_SPARKLINE_MAX_PERIODS = 60;
13812
+ // How a time axis writes its period labels, for the Date labels select.
13813
+ const CHART_DATE_LABEL_OPTIONS = [
13814
+ { value: ReportChartDateLabel.Short, name: 'Short — May' },
13815
+ { value: ReportChartDateLabel.Long, name: 'Long — May 2026' },
13816
+ { value: ReportChartDateLabel.Numeric, name: 'Numeric — 2026-05' },
13817
+ { value: ReportChartDateLabel.PeriodEnd, name: 'Period end — 5/31/2026' },
13818
+ ];
13819
+ // The periods a time axis draws, for the Periods shown select.
13820
+ const CHART_SPAN_MODE_OPTIONS = [
13821
+ { value: ReportChartSpanMode.None, name: 'The date filter\'s range' },
13822
+ { value: ReportChartSpanMode.Periods, name: 'The last periods, up to the date filter\'s end' },
13823
+ { value: ReportChartSpanMode.Start, name: 'Every period since a date, up to the date filter\'s end' },
13824
+ ];
13825
+ // A span's periods stay within 1 to 400, the server's bounds.
13826
+ const CHART_SPAN_MIN_PERIODS = 1;
13827
+ const CHART_SPAN_MAX_PERIODS = 400;
13828
+ const CHART_SPAN_DEFAULT_PERIODS = 3;
13760
13829
  // A percent KPI's target is entered as the percent shown (15 for 15%) and
13761
13830
  // stored as the 0-1 ratio its value arrives in; the server refuses a stored
13762
13831
  // target above 100, which is 10,000% here.
@@ -13777,6 +13846,9 @@ const FORMULA_LINES = {
13777
13846
  // reads. On a derived KPI its card: what a rise means, a target and a
13778
13847
  // sparkline (number formats stay with the AI). Position/size are tuned by
13779
13848
  // dragging on the canvas; height behaviour follows from the component type.
13849
+ // On a chart over time its axis: how the periods are labelled and, on a
13850
+ // derived chart, which periods it draws (a span that ends where the date
13851
+ // filter ends, which the server runs).
13780
13852
  // Filters — ONE listing of every filterable column on this component. Each
13781
13853
  // row carries two toggles: show in the report bar and/or show on this
13782
13854
  // component (both, either, or neither). The interface (date range vs value
@@ -13843,6 +13915,21 @@ class ComponentSettingsComponent {
13843
13915
  sparkline = signal(false);
13844
13916
  sparklineGranularity = signal(KPI_SPARKLINE_DEFAULT_GRANULARITY);
13845
13917
  sparklinePeriods = signal(KPI_SPARKLINE_DEFAULT_PERIODS);
13918
+ // ---- Time axis state (a chart over time only) ----
13919
+ dateLabelOptions = CHART_DATE_LABEL_OPTIONS;
13920
+ spanModeOptions = CHART_SPAN_MODE_OPTIONS;
13921
+ spanMinPeriods = CHART_SPAN_MIN_PERIODS;
13922
+ spanMaxPeriods = CHART_SPAN_MAX_PERIODS;
13923
+ ReportChartSpanMode = ReportChartSpanMode;
13924
+ timeAxis = signal(false);
13925
+ // A span is run by the server on the chart's document, so a chart written
13926
+ // as SQL has none.
13927
+ spanAllowed = signal(false);
13928
+ dateLabel = signal(ReportChartDateLabel.Short);
13929
+ spanMode = signal(ReportChartSpanMode.None);
13930
+ // The periods as typed: fsMaskNumber hands back the unmasked string.
13931
+ spanPeriods = signal(CHART_SPAN_DEFAULT_PERIODS);
13932
+ spanStart = signal(null);
13846
13933
  // ---- SQL tab state ----
13847
13934
  // The statement, once fetched. Null while it is in flight, and null when the
13848
13935
  // component's document could not be compiled (sqlError then says why).
@@ -13897,6 +13984,7 @@ class ComponentSettingsComponent {
13897
13984
  this.paddingLeft = padding?.left ?? DEFAULT_COMPONENT_PADDING;
13898
13985
  this.configJson = JSON.stringify(this.component.config ?? {}, null, 2);
13899
13986
  this._seedKpiCard();
13987
+ this._seedTimeAxis();
13900
13988
  this._seedFilters(this.report, this.component.id);
13901
13989
  // A formula has no statement to compile and no columns to filter on, and
13902
13990
  // the server would refuse both requests.
@@ -13976,23 +14064,38 @@ class ComponentSettingsComponent {
13976
14064
  const kpi = this.kpiCard() ? this._kpiCardSettings() : null;
13977
14065
  const kpiChanged = !!kpi
13978
14066
  && !sameKpiCardSettings(kpi, kpiCardSettings(this.component.config));
14067
+ // A chart's time-axis keys go the same way: dateLabel always, span only
14068
+ // where the chart can take one, so a chart written as SQL is never sent
14069
+ // a key the server refuses.
14070
+ const axis = this.timeAxis() ? this._chartAxisSettings() : null;
14071
+ const axisChanged = !!axis
14072
+ && !sameChartAxisSettings(axis, chartAxisSettings(this.component.config));
13979
14073
  return this._reportData.updateComponent(this.report.id, this.component.id, {
13980
14074
  title: settings.title,
13981
14075
  ...(this.isValue() ? {} : { padding: settings.padding }),
13982
14076
  ...(kpiChanged ? kpi : {}),
14077
+ ...(axisChanged ? this._sentAxisKeys(axis) : {}),
13983
14078
  })
13984
14079
  .pipe(tap((response) => {
13985
- // The card is drawn from the keys the server saved, read back from
13986
- // the config it returns. A server that returns no config did not
13987
- // save them, so the card keeps what it had and the dialog says so.
14080
+ // The card and the axis are drawn from the keys the server saved,
14081
+ // read back from the config it returns. A server that returns no
14082
+ // config did not save them, so the component keeps what it had and
14083
+ // the dialog says so.
13988
14084
  const saved = kpiChanged ? this._savedKpiCard(response) : null;
13989
- if (kpiChanged && !saved) {
13990
- this._message.warning('Component saved, but the server did not save the card settings');
14085
+ const savedAxis = axisChanged ? this._savedChartAxis(response) : null;
14086
+ if ((kpiChanged && !saved) || (axisChanged && !savedAxis)) {
14087
+ this._message.warning('Component saved, but the server did not save the card or axis settings');
13991
14088
  }
13992
14089
  else {
13993
14090
  this._message.success(this.isValue() ? 'Value saved' : 'Component saved');
13994
14091
  }
13995
- this._dialogRef.close({ settings: saved ? { ...settings, kpi: saved } : settings });
14092
+ this._dialogRef.close({
14093
+ settings: {
14094
+ ...settings,
14095
+ ...(saved ? { kpi: saved } : {}),
14096
+ ...(savedAxis ? { chartAxis: savedAxis } : {}),
14097
+ },
14098
+ });
13996
14099
  }));
13997
14100
  };
13998
14101
  // The type a column's filter will be made with. Only a column with no
@@ -14138,6 +14241,52 @@ class ComponentSettingsComponent {
14138
14241
  this.sparklineGranularity.set(config.sparkline?.granularity ?? KPI_SPARKLINE_DEFAULT_GRANULARITY);
14139
14242
  this.sparklinePeriods.set(config.sparkline?.periods ?? KPI_SPARKLINE_DEFAULT_PERIODS);
14140
14243
  }
14244
+ // A chart over time's axis keys, from its config. Only a derived chart
14245
+ // takes a span; a chart with none reads the date filter's range.
14246
+ _seedTimeAxis() {
14247
+ const config = (this.component.config ?? {});
14248
+ const span = config.xAxis?.span;
14249
+ this.timeAxis.set(this.component.type === 'chart' && config.xAxis?.kind === 'time');
14250
+ this.spanAllowed.set(this.timeAxis() && !this.component.sql);
14251
+ this.dateLabel.set(config.xAxis?.dateLabel ?? ReportChartDateLabel.Short);
14252
+ this.spanMode.set(span?.start
14253
+ ? ReportChartSpanMode.Start
14254
+ : (span?.periods ? ReportChartSpanMode.Periods : ReportChartSpanMode.None));
14255
+ this.spanPeriods.set(span?.periods ?? CHART_SPAN_DEFAULT_PERIODS);
14256
+ this.spanStart.set(dateFromBound(span?.start));
14257
+ }
14258
+ // Short is what an absent dateLabel reads, so it is saved as absent. A
14259
+ // chart that cannot take a span keeps the one it has (none).
14260
+ _chartAxisSettings() {
14261
+ return {
14262
+ dateLabel: this.dateLabel() === ReportChartDateLabel.Short ? null : this.dateLabel(),
14263
+ span: this.spanAllowed() ? this._span() : chartAxisSettings(this.component.config).span,
14264
+ };
14265
+ }
14266
+ _span() {
14267
+ const through = ReportChartSpanThrough.FilterEnd;
14268
+ switch (this.spanMode()) {
14269
+ case ReportChartSpanMode.Periods:
14270
+ return { through, periods: this._number(this.spanPeriods()) ?? CHART_SPAN_DEFAULT_PERIODS };
14271
+ case ReportChartSpanMode.Start: {
14272
+ const start = dateToBound(this.spanStart());
14273
+ return start ? { through, start } : null;
14274
+ }
14275
+ default:
14276
+ return null;
14277
+ }
14278
+ }
14279
+ // The axis keys as the settings route takes them: the span only from a
14280
+ // chart that can take one.
14281
+ _sentAxisKeys(axis) {
14282
+ return this.spanAllowed() ? { ...axis } : { dateLabel: axis.dateLabel };
14283
+ }
14284
+ // The axis keys the server saved, from the config its response carries;
14285
+ // null when it carries none.
14286
+ _savedChartAxis(response) {
14287
+ const config = response?.config;
14288
+ return config && typeof config === 'object' ? chartAxisSettings(config) : null;
14289
+ }
14141
14290
  // The card keys the server saved, from the config its response carries;
14142
14291
  // null when it carries none (a server that does not save them).
14143
14292
  _savedKpiCard(response) {
@@ -14296,12 +14445,13 @@ class ComponentSettingsComponent {
14296
14445
  .replace(/\b\w/g, (character) => character.toUpperCase());
14297
14446
  }
14298
14447
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14299
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentSettingsComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "_titleInput", first: true, predicate: ["titleInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ heading() }}\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <!-- A value's Name is just its name. A title may carry the page,\n report and filter fields a text block's words do: Insert\n field puts one in at the caret, and the hint says how the\n title reads where the component is drawn. -->\n @if (isValue()) {\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [ngModel]=\"title()\"\n (ngModelChange)=\"title.set($event)\"\n name=\"title\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n } @else {\n <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n #titleInput\n matInput\n [ngModel]=\"title()\"\n (ngModelChange)=\"title.set($event)\"\n name=\"title\">\n <!-- The button swallows its own click: the form field would\n otherwise put focus back in the input while the menu is\n open, and a key pressed then would move the caret the\n field goes in at. -->\n <button\n mat-icon-button\n matIconSuffix\n type=\"button\"\n aria-label=\"Insert field\"\n [fsMenuTriggerFor]=\"insertFieldMenu\"\n (click)=\"$event.stopPropagation()\">\n <mat-icon>sell</mat-icon>\n </button>\n @if (titleShownAs()) {\n <mat-hint>\n Shows as: {{ titleShownAs() }}\n </mat-hint>\n }\n </mat-form-field>\n <fs-menu #insertFieldMenu>\n @for (entry of titleFields(); track entry.field) {\n <ng-template\n fs-menu-item\n (click)=\"insertField(entry.field)\">\n {{ entry.name }}\n </ng-template>\n }\n </fs-menu>\n }\n <!-- A formula value is read-only beyond its name: what it works\n out is the AI's to change. A formula this viewer cannot\n read has no line, and no row. -->\n @if (formulaLine()) {\n <fs-label-field>\n <fs-label>\n Formula\n </fs-label>\n {{ formulaLine() }}\n <fs-label-message>\n Worked out on the server; it follows the filters of the values it reads.\n </fs-label-message>\n </fs-label-field>\n }\n <!-- A value sits inside a text block's words: no box, no padding. -->\n @if (!isValue()) {\n <div class=\"subheading-2\">\n Padding\n </div>\n <div class=\"geometry-row\">\n <mat-form-field>\n <mat-label>\n Top\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n </div>\n }\n <!-- A derived KPI's card: what a rise means, a target and a\n sparkline. The server computes the progress and the points. -->\n @if (kpiCard()) {\n <div class=\"subheading-2\">\n Card\n </div>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Increase is\n </mat-label>\n <mat-select\n [ngModel]=\"increaseIs()\"\n (ngModelChange)=\"increaseIs.set($event)\"\n name=\"increaseIs\">\n @for (option of kpiPolarityOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Colors the change and decides when the target is met.\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Target\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"6\"\n [signed]=\"true\"\n [ngModel]=\"targetValue()\"\n (ngModelChange)=\"targetValue.set($event)\"\n name=\"targetValue\"\n [validate]=\"validateTarget\">\n @if (targetPercent()) {\n <div matTextSuffix>\n %\n </div>\n }\n <mat-hint>\n The value to reach. Leave empty for no target.\n </mat-hint>\n </mat-form-field>\n @if (hasTarget()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Target label\n </mat-label>\n <input\n matInput\n [ngModel]=\"targetLabel()\"\n (ngModelChange)=\"targetLabel.set($event)\"\n name=\"targetLabel\">\n <mat-hint>\n Shown before the target, such as Goal. Empty reads Target.\n </mat-hint>\n </mat-form-field>\n }\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"sparkline()\"\n (ngModelChange)=\"sparkline.set($event)\"\n name=\"sparkline\">\n Sparkline\n </mat-slide-toggle>\n <mat-hint>\n A small trend line of the value over its last periods.\n </mat-hint>\n </fs-label-field>\n @if (sparkline()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Sparkline period\n </mat-label>\n <mat-select\n [ngModel]=\"sparklineGranularity()\"\n (ngModelChange)=\"sparklineGranularity.set($event)\"\n name=\"sparklineGranularity\">\n @for (option of granularityOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Sparkline periods\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"0\"\n [ngModel]=\"sparklinePeriods()\"\n (ngModelChange)=\"sparklinePeriods.set($event)\"\n name=\"sparklinePeriods\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"sparklineMinPeriods\"\n [fsFormMax]=\"sparklineMaxPeriods\">\n <mat-hint>\n How many periods the line covers, {{ sparklineMinPeriods }} to {{ sparklineMaxPeriods }}.\n </mat-hint>\n </mat-form-field>\n }\n }\n </div>\n </mat-tab>\n <!-- A formula runs no statement and follows the filters of the values\n it reads, so it has neither a Filters nor an SQL tab. -->\n @if (!isFormula()) {\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"tab-body\">\n @if (columnsError()) {\n <div class=\"filters-empty error\">\n {{ columnsError() }}\n </div>\n } @else if (rows().length) {\n <!-- Every filterable column. Pick where its control shows: in\n the report bar (every page, or this page only), on this\n component, both, or neither. The interface follows the\n data; a numeric column can be values or a number range.\n A value sits inside text and has no component to show a\n control on, so it gets the report bar column only. -->\n <table class=\"filter-table\">\n <thead>\n <tr>\n <th class=\"filter-col-name\">\n Filter\n </th>\n <th class=\"filter-col-type\">\n Type\n </th>\n <th class=\"filter-col-select\">\n Report bar\n </th>\n @if (!isValue()) {\n <th class=\"filter-col-toggle\">\n This component\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.column) {\n <tr>\n <td class=\"filter-col-name\">\n {{ row.label }}\n </td>\n <td class=\"filter-col-type\">\n @if (row.typeSelect.show) {\n <mat-form-field\n [subscriptSizing]=\"'dynamic'\"\n [matTooltip]=\"row.typeSelect.tooltip\">\n <mat-select\n [panelWidth]=\"null\"\n [ngModel]=\"row.type\"\n (ngModelChange)=\"setType(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [disabled]=\"row.typeSelect.disabled\"\n aria-label=\"Type\">\n @for (option of typeOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n {{ typeLabel(row.type) }}\n }\n </td>\n <td class=\"filter-col-select\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-select\n [panelWidth]=\"null\"\n [ngModel]=\"row.placement\"\n (ngModelChange)=\"setPlacement(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n aria-label=\"Report bar\">\n @for (option of row.placementOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </td>\n @if (!isValue()) {\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setComponent(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [disabled]=\"row.componentToggle.disabled\"\n [matTooltip]=\"row.componentToggle.tooltip\">\n </mat-slide-toggle>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <div class=\"filters-empty\">\n This component has no filterable columns yet \u2014 give it some SQL first.\n </div>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"SQL\"\n name=\"sql\">\n <div class=\"tab-body\">\n @if (sqlError()) {\n <div class=\"filters-empty error\">\n {{ sqlError() }}\n </div>\n } @else if (sql()) {\n @if (sqlBaseline()) {\n <!-- A derived component's statement is compiled from its\n document with no viewer state, so it is not byte-for-byte\n what produced the numbers on screen. Said here rather than\n left for the reader to discover. -->\n <div class=\"filters-empty\">\n Compiled from this component's query \u2014 no filters applied, times in {{ sqlTimezone() }}.\n </div>\n }\n <pre class=\"code-block\">{{ sql() }}</pre>\n } @else {\n <div class=\"filters-empty\">\n This component has no SQL yet.\n </div>\n }\n </div>\n </mat-tab>\n }\n <mat-tab\n label=\"Config\"\n name=\"config\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ configJson }}\n </pre>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- Save | Delete | Cancel, the standard order. fs-form-dialog-actions\n renders its own Save/Done BEFORE the projected content, which would\n put Delete after Cancel, so `[save]=\"false\"` foots the row here and\n all three buttons are projected in order. Save submits the Settings\n tab; the other tabs persist as you touch them, so they get Done. -->\n <fs-form-dialog-actions [save]=\"false\">\n @if (selectedTab === 'settings') {\n <button\n mat-button\n type=\"submit\"\n color=\"primary\">\n Save\n </button>\n } @else {\n <button\n mat-button\n type=\"button\"\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Done\n </button>\n }\n <button\n mat-button\n type=\"button\"\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n <button\n mat-button\n type=\"button\"\n class=\"cancel-button\"\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}.geometry-row{display:flex;gap:8px}.geometry-row mat-form-field{flex:1}.filter-table{border-spacing:0;width:100%;border-collapse:collapse}.filter-table thead th{color:#8f8f8f;font-weight:400;font-size:85%;text-align:left;padding:8px 16px}.filter-table tbody tr{clip-path:xywh(0 3px 100% calc(100% - 6px) round 10px)}.filter-table tbody td{border:none;background-color:#fafafa;padding:8px 16px;vertical-align:middle;text-align:left}.filter-table .filter-col-name{font-weight:600;color:#1f2933}.filter-table .filter-col-type{color:#7b8794;width:150px}.filter-table .filter-col-select{width:160px}.filter-table td.filter-col-type,.filter-table td.filter-col-select{padding-left:8px;padding-right:8px}.filter-table td.filter-col-type mat-form-field,.filter-table td.filter-col-select mat-form-field{width:100%;--mat-form-field-container-height: 36px;--mat-form-field-container-vertical-padding: 6px}.filter-table td.filter-col-type ::ng-deep .mat-mdc-form-field-infix,.filter-table td.filter-col-select ::ng-deep .mat-mdc-form-field-infix{width:auto}.filter-table td.filter-col-type ::ng-deep .mat-mdc-form-field-subscript-wrapper,.filter-table td.filter-col-select ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.filter-table .filter-col-toggle,.filter-table th.filter-col-toggle{width:110px;text-align:center}.filters-empty{font-size:13px;color:#7b8794;padding:8px 0 4px}.filters-empty.error{color:#e15759}.code-block{margin:0;padding:10px 12px;background:#1f2933;color:#e4e7eb;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$2.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i2$2.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$2.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$2.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "directive", type: i2$2.FsFormIntegerDirective, selector: "[fsFormInteger]", inputs: ["fsFormInteger", "fsFormIntegerMessage"] }, { kind: "directive", type: i2$2.FsFormValidateDirective, selector: "[validate]", inputs: ["validate", "validateData", "validateOnSubmit"] }, { kind: "component", type: i2$2.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$2.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i2$2.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsMaskModule }, { kind: "directive", type: i3$1.FsMaskNumberDirective, selector: "[fsMaskNumber]" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "component", type: i5$1.FsLabelComponent, selector: "fs-label" }, { kind: "component", type: i5$1.FsLabelFieldComponent, selector: "fs-label-field", inputs: ["appearance", "showOutline", "disabled", "focused", "hoverable", "padless"] }, { kind: "component", type: i5$1.FsLabelMessageComponent, selector: "fs-label-message" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i6.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i6.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$4.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$4.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$4.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14448
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentSettingsComponent, isStandalone: true, selector: "ng-component", viewQueries: [{ propertyName: "_titleInput", first: true, predicate: ["titleInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ heading() }}\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <!-- A value's Name is just its name. A title may carry the page,\n report and filter fields a text block's words do: Insert\n field puts one in at the caret, and the hint says how the\n title reads where the component is drawn. -->\n @if (isValue()) {\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [ngModel]=\"title()\"\n (ngModelChange)=\"title.set($event)\"\n name=\"title\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n } @else {\n <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n #titleInput\n matInput\n [ngModel]=\"title()\"\n (ngModelChange)=\"title.set($event)\"\n name=\"title\">\n <!-- The button swallows its own click: the form field would\n otherwise put focus back in the input while the menu is\n open, and a key pressed then would move the caret the\n field goes in at. -->\n <button\n mat-icon-button\n matIconSuffix\n type=\"button\"\n aria-label=\"Insert field\"\n [fsMenuTriggerFor]=\"insertFieldMenu\"\n (click)=\"$event.stopPropagation()\">\n <mat-icon>sell</mat-icon>\n </button>\n @if (titleShownAs()) {\n <mat-hint>\n Shows as: {{ titleShownAs() }}\n </mat-hint>\n }\n </mat-form-field>\n <fs-menu #insertFieldMenu>\n @for (entry of titleFields(); track entry.field) {\n <ng-template\n fs-menu-item\n (click)=\"insertField(entry.field)\">\n {{ entry.name }}\n </ng-template>\n }\n </fs-menu>\n }\n <!-- A formula value is read-only beyond its name: what it works\n out is the AI's to change. A formula this viewer cannot\n read has no line, and no row. -->\n @if (formulaLine()) {\n <fs-label-field>\n <fs-label>\n Formula\n </fs-label>\n {{ formulaLine() }}\n <fs-label-message>\n Worked out on the server; it follows the filters of the values it reads.\n </fs-label-message>\n </fs-label-field>\n }\n <!-- A value sits inside a text block's words: no box, no padding. -->\n @if (!isValue()) {\n <div class=\"subheading-2\">\n Padding\n </div>\n <div class=\"geometry-row\">\n <mat-form-field>\n <mat-label>\n Top\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n </div>\n }\n <!-- A derived KPI's card: what a rise means, a target and a\n sparkline. The server computes the progress and the points. -->\n @if (kpiCard()) {\n <div class=\"subheading-2\">\n Card\n </div>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Increase is\n </mat-label>\n <mat-select\n [ngModel]=\"increaseIs()\"\n (ngModelChange)=\"increaseIs.set($event)\"\n name=\"increaseIs\">\n @for (option of kpiPolarityOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Colors the change and decides when the target is met.\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Target\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"6\"\n [signed]=\"true\"\n [ngModel]=\"targetValue()\"\n (ngModelChange)=\"targetValue.set($event)\"\n name=\"targetValue\"\n [validate]=\"validateTarget\">\n @if (targetPercent()) {\n <div matTextSuffix>\n %\n </div>\n }\n <mat-hint>\n The value to reach. Leave empty for no target.\n </mat-hint>\n </mat-form-field>\n @if (hasTarget()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Target label\n </mat-label>\n <input\n matInput\n [ngModel]=\"targetLabel()\"\n (ngModelChange)=\"targetLabel.set($event)\"\n name=\"targetLabel\">\n <mat-hint>\n Shown before the target, such as Goal. Empty reads Target.\n </mat-hint>\n </mat-form-field>\n }\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"sparkline()\"\n (ngModelChange)=\"sparkline.set($event)\"\n name=\"sparkline\">\n Sparkline\n </mat-slide-toggle>\n <mat-hint>\n A small trend line of the value over its last periods.\n </mat-hint>\n </fs-label-field>\n @if (sparkline()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Sparkline period\n </mat-label>\n <mat-select\n [ngModel]=\"sparklineGranularity()\"\n (ngModelChange)=\"sparklineGranularity.set($event)\"\n name=\"sparklineGranularity\">\n @for (option of granularityOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Sparkline periods\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"0\"\n [ngModel]=\"sparklinePeriods()\"\n (ngModelChange)=\"sparklinePeriods.set($event)\"\n name=\"sparklinePeriods\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"sparklineMinPeriods\"\n [fsFormMax]=\"sparklineMaxPeriods\">\n <mat-hint>\n How many periods the line covers, {{ sparklineMinPeriods }} to {{ sparklineMaxPeriods }}.\n </mat-hint>\n </mat-form-field>\n }\n }\n <!-- A chart over time: how its periods are labelled and, on a\n derived chart, which periods it draws. The server runs the\n span, so the screen and every export draw the same periods;\n the KPIs and values on the page keep the date filter's range. -->\n @if (timeAxis()) {\n <div class=\"subheading-2\">\n Time axis\n </div>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Date labels\n </mat-label>\n <mat-select\n [ngModel]=\"dateLabel()\"\n (ngModelChange)=\"dateLabel.set($event)\"\n name=\"dateLabel\">\n @for (option of dateLabelOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Period end labels each period by its last day: a week by its Sunday.\n </mat-hint>\n </mat-form-field>\n @if (spanAllowed()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Periods shown\n </mat-label>\n <mat-select\n [ngModel]=\"spanMode()\"\n (ngModelChange)=\"spanMode.set($event)\"\n name=\"spanMode\">\n @for (option of spanModeOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n With no date filter set, the chart ends at the current period.\n </mat-hint>\n </mat-form-field>\n @if (spanMode() === ReportChartSpanMode.Periods) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Periods\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"0\"\n [ngModel]=\"spanPeriods()\"\n (ngModelChange)=\"spanPeriods.set($event)\"\n name=\"spanPeriods\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"spanMinPeriods\"\n [fsFormMax]=\"spanMaxPeriods\">\n <mat-hint>\n How many periods the chart draws, {{ spanMinPeriods }} to {{ spanMaxPeriods }}; 3 on a monthly chart is the last three months.\n </mat-hint>\n </mat-form-field>\n } @else if (spanMode() === ReportChartSpanMode.Start) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Since\n </mat-label>\n <input\n matInput\n fsDatePicker\n [ngModel]=\"spanStart()\"\n (ngModelChange)=\"spanStart.set($event)\"\n name=\"spanStart\"\n [fsFormRequired]=\"true\">\n <mat-hint>\n The chart draws every period from this day, such as a go-live.\n </mat-hint>\n </mat-form-field>\n }\n }\n }\n </div>\n </mat-tab>\n <!-- A formula runs no statement and follows the filters of the values\n it reads, so it has neither a Filters nor an SQL tab. -->\n @if (!isFormula()) {\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"tab-body\">\n @if (columnsError()) {\n <div class=\"filters-empty error\">\n {{ columnsError() }}\n </div>\n } @else if (rows().length) {\n <!-- Every filterable column. Pick where its control shows: in\n the report bar (every page, or this page only), on this\n component, both, or neither. The interface follows the\n data; a numeric column can be values or a number range.\n A value sits inside text and has no component to show a\n control on, so it gets the report bar column only. -->\n <table class=\"filter-table\">\n <thead>\n <tr>\n <th class=\"filter-col-name\">\n Filter\n </th>\n <th class=\"filter-col-type\">\n Type\n </th>\n <th class=\"filter-col-select\">\n Report bar\n </th>\n @if (!isValue()) {\n <th class=\"filter-col-toggle\">\n This component\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.column) {\n <tr>\n <td class=\"filter-col-name\">\n {{ row.label }}\n </td>\n <td class=\"filter-col-type\">\n @if (row.typeSelect.show) {\n <mat-form-field\n [subscriptSizing]=\"'dynamic'\"\n [matTooltip]=\"row.typeSelect.tooltip\">\n <mat-select\n [panelWidth]=\"null\"\n [ngModel]=\"row.type\"\n (ngModelChange)=\"setType(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [disabled]=\"row.typeSelect.disabled\"\n aria-label=\"Type\">\n @for (option of typeOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n {{ typeLabel(row.type) }}\n }\n </td>\n <td class=\"filter-col-select\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-select\n [panelWidth]=\"null\"\n [ngModel]=\"row.placement\"\n (ngModelChange)=\"setPlacement(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n aria-label=\"Report bar\">\n @for (option of row.placementOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </td>\n @if (!isValue()) {\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setComponent(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [disabled]=\"row.componentToggle.disabled\"\n [matTooltip]=\"row.componentToggle.tooltip\">\n </mat-slide-toggle>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <div class=\"filters-empty\">\n This component has no filterable columns yet \u2014 give it some SQL first.\n </div>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"SQL\"\n name=\"sql\">\n <div class=\"tab-body\">\n @if (sqlError()) {\n <div class=\"filters-empty error\">\n {{ sqlError() }}\n </div>\n } @else if (sql()) {\n @if (sqlBaseline()) {\n <!-- A derived component's statement is compiled from its\n document with no viewer state, so it is not byte-for-byte\n what produced the numbers on screen. Said here rather than\n left for the reader to discover. -->\n <div class=\"filters-empty\">\n Compiled from this component's query \u2014 no filters applied, times in {{ sqlTimezone() }}.\n </div>\n }\n <pre class=\"code-block\">{{ sql() }}</pre>\n } @else {\n <div class=\"filters-empty\">\n This component has no SQL yet.\n </div>\n }\n </div>\n </mat-tab>\n }\n <mat-tab\n label=\"Config\"\n name=\"config\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ configJson }}\n </pre>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- Save | Delete | Cancel, the standard order. fs-form-dialog-actions\n renders its own Save/Done BEFORE the projected content, which would\n put Delete after Cancel, so `[save]=\"false\"` foots the row here and\n all three buttons are projected in order. Save submits the Settings\n tab; the other tabs persist as you touch them, so they get Done. -->\n <fs-form-dialog-actions [save]=\"false\">\n @if (selectedTab === 'settings') {\n <button\n mat-button\n type=\"submit\"\n color=\"primary\">\n Save\n </button>\n } @else {\n <button\n mat-button\n type=\"button\"\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Done\n </button>\n }\n <button\n mat-button\n type=\"button\"\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n <button\n mat-button\n type=\"button\"\n class=\"cancel-button\"\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}.geometry-row{display:flex;gap:8px}.geometry-row mat-form-field{flex:1}.filter-table{border-spacing:0;width:100%;border-collapse:collapse}.filter-table thead th{color:#8f8f8f;font-weight:400;font-size:85%;text-align:left;padding:8px 16px}.filter-table tbody tr{clip-path:xywh(0 3px 100% calc(100% - 6px) round 10px)}.filter-table tbody td{border:none;background-color:#fafafa;padding:8px 16px;vertical-align:middle;text-align:left}.filter-table .filter-col-name{font-weight:600;color:#1f2933}.filter-table .filter-col-type{color:#7b8794;width:150px}.filter-table .filter-col-select{width:160px}.filter-table td.filter-col-type,.filter-table td.filter-col-select{padding-left:8px;padding-right:8px}.filter-table td.filter-col-type mat-form-field,.filter-table td.filter-col-select mat-form-field{width:100%;--mat-form-field-container-height: 36px;--mat-form-field-container-vertical-padding: 6px}.filter-table td.filter-col-type ::ng-deep .mat-mdc-form-field-infix,.filter-table td.filter-col-select ::ng-deep .mat-mdc-form-field-infix{width:auto}.filter-table td.filter-col-type ::ng-deep .mat-mdc-form-field-subscript-wrapper,.filter-table td.filter-col-select ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.filter-table .filter-col-toggle,.filter-table th.filter-col-toggle{width:110px;text-align:center}.filters-empty{font-size:13px;color:#7b8794;padding:8px 0 4px}.filters-empty.error{color:#e15759}.code-block{margin:0;padding:10px 12px;background:#1f2933;color:#e4e7eb;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i2$4.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i3.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i3.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i3.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i3.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "directive", type: i3.FsFormIntegerDirective, selector: "[fsFormInteger]", inputs: ["fsFormInteger", "fsFormIntegerMessage"] }, { kind: "directive", type: i3.FsFormValidateDirective, selector: "[validate]", inputs: ["validate", "validateData", "validateOnSubmit"] }, { kind: "component", type: i3.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i3.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i3.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsMaskModule }, { kind: "directive", type: i4$3.FsMaskNumberDirective, selector: "[fsMaskNumber]" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "component", type: i6$1.FsLabelComponent, selector: "fs-label" }, { kind: "component", type: i6$1.FsLabelFieldComponent, selector: "fs-label-field", inputs: ["appearance", "showOutline", "disabled", "focused", "hoverable", "padless"] }, { kind: "component", type: i6$1.FsLabelMessageComponent, selector: "fs-label-message" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i7.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i7.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i6.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i6.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: MatDialogClose, selector: "[mat-dialog-close], [matDialogClose]", inputs: ["aria-label", "type", "mat-dialog-close", "matDialogClose"], exportAs: ["matDialogClose"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14300
14449
  }
14301
14450
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentSettingsComponent, decorators: [{
14302
14451
  type: Component,
14303
14452
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
14304
14453
  FormsModule,
14454
+ FsDatePickerModule,
14305
14455
  FsFormModule,
14306
14456
  FsMaskModule,
14307
14457
  FsDialogModule,
@@ -14325,7 +14475,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
14325
14475
  MatSlideToggle,
14326
14476
  MatTooltip,
14327
14477
  FsMenuModule,
14328
- ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ heading() }}\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <!-- A value's Name is just its name. A title may carry the page,\n report and filter fields a text block's words do: Insert\n field puts one in at the caret, and the hint says how the\n title reads where the component is drawn. -->\n @if (isValue()) {\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [ngModel]=\"title()\"\n (ngModelChange)=\"title.set($event)\"\n name=\"title\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n } @else {\n <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n #titleInput\n matInput\n [ngModel]=\"title()\"\n (ngModelChange)=\"title.set($event)\"\n name=\"title\">\n <!-- The button swallows its own click: the form field would\n otherwise put focus back in the input while the menu is\n open, and a key pressed then would move the caret the\n field goes in at. -->\n <button\n mat-icon-button\n matIconSuffix\n type=\"button\"\n aria-label=\"Insert field\"\n [fsMenuTriggerFor]=\"insertFieldMenu\"\n (click)=\"$event.stopPropagation()\">\n <mat-icon>sell</mat-icon>\n </button>\n @if (titleShownAs()) {\n <mat-hint>\n Shows as: {{ titleShownAs() }}\n </mat-hint>\n }\n </mat-form-field>\n <fs-menu #insertFieldMenu>\n @for (entry of titleFields(); track entry.field) {\n <ng-template\n fs-menu-item\n (click)=\"insertField(entry.field)\">\n {{ entry.name }}\n </ng-template>\n }\n </fs-menu>\n }\n <!-- A formula value is read-only beyond its name: what it works\n out is the AI's to change. A formula this viewer cannot\n read has no line, and no row. -->\n @if (formulaLine()) {\n <fs-label-field>\n <fs-label>\n Formula\n </fs-label>\n {{ formulaLine() }}\n <fs-label-message>\n Worked out on the server; it follows the filters of the values it reads.\n </fs-label-message>\n </fs-label-field>\n }\n <!-- A value sits inside a text block's words: no box, no padding. -->\n @if (!isValue()) {\n <div class=\"subheading-2\">\n Padding\n </div>\n <div class=\"geometry-row\">\n <mat-form-field>\n <mat-label>\n Top\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n </div>\n }\n <!-- A derived KPI's card: what a rise means, a target and a\n sparkline. The server computes the progress and the points. -->\n @if (kpiCard()) {\n <div class=\"subheading-2\">\n Card\n </div>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Increase is\n </mat-label>\n <mat-select\n [ngModel]=\"increaseIs()\"\n (ngModelChange)=\"increaseIs.set($event)\"\n name=\"increaseIs\">\n @for (option of kpiPolarityOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Colors the change and decides when the target is met.\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Target\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"6\"\n [signed]=\"true\"\n [ngModel]=\"targetValue()\"\n (ngModelChange)=\"targetValue.set($event)\"\n name=\"targetValue\"\n [validate]=\"validateTarget\">\n @if (targetPercent()) {\n <div matTextSuffix>\n %\n </div>\n }\n <mat-hint>\n The value to reach. Leave empty for no target.\n </mat-hint>\n </mat-form-field>\n @if (hasTarget()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Target label\n </mat-label>\n <input\n matInput\n [ngModel]=\"targetLabel()\"\n (ngModelChange)=\"targetLabel.set($event)\"\n name=\"targetLabel\">\n <mat-hint>\n Shown before the target, such as Goal. Empty reads Target.\n </mat-hint>\n </mat-form-field>\n }\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"sparkline()\"\n (ngModelChange)=\"sparkline.set($event)\"\n name=\"sparkline\">\n Sparkline\n </mat-slide-toggle>\n <mat-hint>\n A small trend line of the value over its last periods.\n </mat-hint>\n </fs-label-field>\n @if (sparkline()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Sparkline period\n </mat-label>\n <mat-select\n [ngModel]=\"sparklineGranularity()\"\n (ngModelChange)=\"sparklineGranularity.set($event)\"\n name=\"sparklineGranularity\">\n @for (option of granularityOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Sparkline periods\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"0\"\n [ngModel]=\"sparklinePeriods()\"\n (ngModelChange)=\"sparklinePeriods.set($event)\"\n name=\"sparklinePeriods\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"sparklineMinPeriods\"\n [fsFormMax]=\"sparklineMaxPeriods\">\n <mat-hint>\n How many periods the line covers, {{ sparklineMinPeriods }} to {{ sparklineMaxPeriods }}.\n </mat-hint>\n </mat-form-field>\n }\n }\n </div>\n </mat-tab>\n <!-- A formula runs no statement and follows the filters of the values\n it reads, so it has neither a Filters nor an SQL tab. -->\n @if (!isFormula()) {\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"tab-body\">\n @if (columnsError()) {\n <div class=\"filters-empty error\">\n {{ columnsError() }}\n </div>\n } @else if (rows().length) {\n <!-- Every filterable column. Pick where its control shows: in\n the report bar (every page, or this page only), on this\n component, both, or neither. The interface follows the\n data; a numeric column can be values or a number range.\n A value sits inside text and has no component to show a\n control on, so it gets the report bar column only. -->\n <table class=\"filter-table\">\n <thead>\n <tr>\n <th class=\"filter-col-name\">\n Filter\n </th>\n <th class=\"filter-col-type\">\n Type\n </th>\n <th class=\"filter-col-select\">\n Report bar\n </th>\n @if (!isValue()) {\n <th class=\"filter-col-toggle\">\n This component\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.column) {\n <tr>\n <td class=\"filter-col-name\">\n {{ row.label }}\n </td>\n <td class=\"filter-col-type\">\n @if (row.typeSelect.show) {\n <mat-form-field\n [subscriptSizing]=\"'dynamic'\"\n [matTooltip]=\"row.typeSelect.tooltip\">\n <mat-select\n [panelWidth]=\"null\"\n [ngModel]=\"row.type\"\n (ngModelChange)=\"setType(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [disabled]=\"row.typeSelect.disabled\"\n aria-label=\"Type\">\n @for (option of typeOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n {{ typeLabel(row.type) }}\n }\n </td>\n <td class=\"filter-col-select\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-select\n [panelWidth]=\"null\"\n [ngModel]=\"row.placement\"\n (ngModelChange)=\"setPlacement(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n aria-label=\"Report bar\">\n @for (option of row.placementOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </td>\n @if (!isValue()) {\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setComponent(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [disabled]=\"row.componentToggle.disabled\"\n [matTooltip]=\"row.componentToggle.tooltip\">\n </mat-slide-toggle>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <div class=\"filters-empty\">\n This component has no filterable columns yet \u2014 give it some SQL first.\n </div>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"SQL\"\n name=\"sql\">\n <div class=\"tab-body\">\n @if (sqlError()) {\n <div class=\"filters-empty error\">\n {{ sqlError() }}\n </div>\n } @else if (sql()) {\n @if (sqlBaseline()) {\n <!-- A derived component's statement is compiled from its\n document with no viewer state, so it is not byte-for-byte\n what produced the numbers on screen. Said here rather than\n left for the reader to discover. -->\n <div class=\"filters-empty\">\n Compiled from this component's query \u2014 no filters applied, times in {{ sqlTimezone() }}.\n </div>\n }\n <pre class=\"code-block\">{{ sql() }}</pre>\n } @else {\n <div class=\"filters-empty\">\n This component has no SQL yet.\n </div>\n }\n </div>\n </mat-tab>\n }\n <mat-tab\n label=\"Config\"\n name=\"config\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ configJson }}\n </pre>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- Save | Delete | Cancel, the standard order. fs-form-dialog-actions\n renders its own Save/Done BEFORE the projected content, which would\n put Delete after Cancel, so `[save]=\"false\"` foots the row here and\n all three buttons are projected in order. Save submits the Settings\n tab; the other tabs persist as you touch them, so they get Done. -->\n <fs-form-dialog-actions [save]=\"false\">\n @if (selectedTab === 'settings') {\n <button\n mat-button\n type=\"submit\"\n color=\"primary\">\n Save\n </button>\n } @else {\n <button\n mat-button\n type=\"button\"\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Done\n </button>\n }\n <button\n mat-button\n type=\"button\"\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n <button\n mat-button\n type=\"button\"\n class=\"cancel-button\"\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}.geometry-row{display:flex;gap:8px}.geometry-row mat-form-field{flex:1}.filter-table{border-spacing:0;width:100%;border-collapse:collapse}.filter-table thead th{color:#8f8f8f;font-weight:400;font-size:85%;text-align:left;padding:8px 16px}.filter-table tbody tr{clip-path:xywh(0 3px 100% calc(100% - 6px) round 10px)}.filter-table tbody td{border:none;background-color:#fafafa;padding:8px 16px;vertical-align:middle;text-align:left}.filter-table .filter-col-name{font-weight:600;color:#1f2933}.filter-table .filter-col-type{color:#7b8794;width:150px}.filter-table .filter-col-select{width:160px}.filter-table td.filter-col-type,.filter-table td.filter-col-select{padding-left:8px;padding-right:8px}.filter-table td.filter-col-type mat-form-field,.filter-table td.filter-col-select mat-form-field{width:100%;--mat-form-field-container-height: 36px;--mat-form-field-container-vertical-padding: 6px}.filter-table td.filter-col-type ::ng-deep .mat-mdc-form-field-infix,.filter-table td.filter-col-select ::ng-deep .mat-mdc-form-field-infix{width:auto}.filter-table td.filter-col-type ::ng-deep .mat-mdc-form-field-subscript-wrapper,.filter-table td.filter-col-select ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.filter-table .filter-col-toggle,.filter-table th.filter-col-toggle{width:110px;text-align:center}.filters-empty{font-size:13px;color:#7b8794;padding:8px 0 4px}.filters-empty.error{color:#e15759}.code-block{margin:0;padding:10px 12px;background:#1f2933;color:#e4e7eb;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto}\n"] }]
14478
+ ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ heading() }}\n </h1>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <!-- A value's Name is just its name. A title may carry the page,\n report and filter fields a text block's words do: Insert\n field puts one in at the caret, and the hint says how the\n title reads where the component is drawn. -->\n @if (isValue()) {\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [ngModel]=\"title()\"\n (ngModelChange)=\"title.set($event)\"\n name=\"title\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n } @else {\n <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n #titleInput\n matInput\n [ngModel]=\"title()\"\n (ngModelChange)=\"title.set($event)\"\n name=\"title\">\n <!-- The button swallows its own click: the form field would\n otherwise put focus back in the input while the menu is\n open, and a key pressed then would move the caret the\n field goes in at. -->\n <button\n mat-icon-button\n matIconSuffix\n type=\"button\"\n aria-label=\"Insert field\"\n [fsMenuTriggerFor]=\"insertFieldMenu\"\n (click)=\"$event.stopPropagation()\">\n <mat-icon>sell</mat-icon>\n </button>\n @if (titleShownAs()) {\n <mat-hint>\n Shows as: {{ titleShownAs() }}\n </mat-hint>\n }\n </mat-form-field>\n <fs-menu #insertFieldMenu>\n @for (entry of titleFields(); track entry.field) {\n <ng-template\n fs-menu-item\n (click)=\"insertField(entry.field)\">\n {{ entry.name }}\n </ng-template>\n }\n </fs-menu>\n }\n <!-- A formula value is read-only beyond its name: what it works\n out is the AI's to change. A formula this viewer cannot\n read has no line, and no row. -->\n @if (formulaLine()) {\n <fs-label-field>\n <fs-label>\n Formula\n </fs-label>\n {{ formulaLine() }}\n <fs-label-message>\n Worked out on the server; it follows the filters of the values it reads.\n </fs-label-message>\n </fs-label-field>\n }\n <!-- A value sits inside a text block's words: no box, no padding. -->\n @if (!isValue()) {\n <div class=\"subheading-2\">\n Padding\n </div>\n <div class=\"geometry-row\">\n <mat-form-field>\n <mat-label>\n Top\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"3\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\"\n [fsFormMin]=\"0\">\n <div matTextSuffix>\n in\n </div>\n </mat-form-field>\n </div>\n }\n <!-- A derived KPI's card: what a rise means, a target and a\n sparkline. The server computes the progress and the points. -->\n @if (kpiCard()) {\n <div class=\"subheading-2\">\n Card\n </div>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Increase is\n </mat-label>\n <mat-select\n [ngModel]=\"increaseIs()\"\n (ngModelChange)=\"increaseIs.set($event)\"\n name=\"increaseIs\">\n @for (option of kpiPolarityOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Colors the change and decides when the target is met.\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Target\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"6\"\n [signed]=\"true\"\n [ngModel]=\"targetValue()\"\n (ngModelChange)=\"targetValue.set($event)\"\n name=\"targetValue\"\n [validate]=\"validateTarget\">\n @if (targetPercent()) {\n <div matTextSuffix>\n %\n </div>\n }\n <mat-hint>\n The value to reach. Leave empty for no target.\n </mat-hint>\n </mat-form-field>\n @if (hasTarget()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Target label\n </mat-label>\n <input\n matInput\n [ngModel]=\"targetLabel()\"\n (ngModelChange)=\"targetLabel.set($event)\"\n name=\"targetLabel\">\n <mat-hint>\n Shown before the target, such as Goal. Empty reads Target.\n </mat-hint>\n </mat-form-field>\n }\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"sparkline()\"\n (ngModelChange)=\"sparkline.set($event)\"\n name=\"sparkline\">\n Sparkline\n </mat-slide-toggle>\n <mat-hint>\n A small trend line of the value over its last periods.\n </mat-hint>\n </fs-label-field>\n @if (sparkline()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Sparkline period\n </mat-label>\n <mat-select\n [ngModel]=\"sparklineGranularity()\"\n (ngModelChange)=\"sparklineGranularity.set($event)\"\n name=\"sparklineGranularity\">\n @for (option of granularityOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Sparkline periods\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"0\"\n [ngModel]=\"sparklinePeriods()\"\n (ngModelChange)=\"sparklinePeriods.set($event)\"\n name=\"sparklinePeriods\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"sparklineMinPeriods\"\n [fsFormMax]=\"sparklineMaxPeriods\">\n <mat-hint>\n How many periods the line covers, {{ sparklineMinPeriods }} to {{ sparklineMaxPeriods }}.\n </mat-hint>\n </mat-form-field>\n }\n }\n <!-- A chart over time: how its periods are labelled and, on a\n derived chart, which periods it draws. The server runs the\n span, so the screen and every export draw the same periods;\n the KPIs and values on the page keep the date filter's range. -->\n @if (timeAxis()) {\n <div class=\"subheading-2\">\n Time axis\n </div>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Date labels\n </mat-label>\n <mat-select\n [ngModel]=\"dateLabel()\"\n (ngModelChange)=\"dateLabel.set($event)\"\n name=\"dateLabel\">\n @for (option of dateLabelOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Period end labels each period by its last day: a week by its Sunday.\n </mat-hint>\n </mat-form-field>\n @if (spanAllowed()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Periods shown\n </mat-label>\n <mat-select\n [ngModel]=\"spanMode()\"\n (ngModelChange)=\"spanMode.set($event)\"\n name=\"spanMode\">\n @for (option of spanModeOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n With no date filter set, the chart ends at the current period.\n </mat-hint>\n </mat-form-field>\n @if (spanMode() === ReportChartSpanMode.Periods) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Periods\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"0\"\n [ngModel]=\"spanPeriods()\"\n (ngModelChange)=\"spanPeriods.set($event)\"\n name=\"spanPeriods\"\n [fsFormRequired]=\"true\"\n [fsFormInteger]=\"true\"\n [fsFormMin]=\"spanMinPeriods\"\n [fsFormMax]=\"spanMaxPeriods\">\n <mat-hint>\n How many periods the chart draws, {{ spanMinPeriods }} to {{ spanMaxPeriods }}; 3 on a monthly chart is the last three months.\n </mat-hint>\n </mat-form-field>\n } @else if (spanMode() === ReportChartSpanMode.Start) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Since\n </mat-label>\n <input\n matInput\n fsDatePicker\n [ngModel]=\"spanStart()\"\n (ngModelChange)=\"spanStart.set($event)\"\n name=\"spanStart\"\n [fsFormRequired]=\"true\">\n <mat-hint>\n The chart draws every period from this day, such as a go-live.\n </mat-hint>\n </mat-form-field>\n }\n }\n }\n </div>\n </mat-tab>\n <!-- A formula runs no statement and follows the filters of the values\n it reads, so it has neither a Filters nor an SQL tab. -->\n @if (!isFormula()) {\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"tab-body\">\n @if (columnsError()) {\n <div class=\"filters-empty error\">\n {{ columnsError() }}\n </div>\n } @else if (rows().length) {\n <!-- Every filterable column. Pick where its control shows: in\n the report bar (every page, or this page only), on this\n component, both, or neither. The interface follows the\n data; a numeric column can be values or a number range.\n A value sits inside text and has no component to show a\n control on, so it gets the report bar column only. -->\n <table class=\"filter-table\">\n <thead>\n <tr>\n <th class=\"filter-col-name\">\n Filter\n </th>\n <th class=\"filter-col-type\">\n Type\n </th>\n <th class=\"filter-col-select\">\n Report bar\n </th>\n @if (!isValue()) {\n <th class=\"filter-col-toggle\">\n This component\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of rows(); track row.column) {\n <tr>\n <td class=\"filter-col-name\">\n {{ row.label }}\n </td>\n <td class=\"filter-col-type\">\n @if (row.typeSelect.show) {\n <mat-form-field\n [subscriptSizing]=\"'dynamic'\"\n [matTooltip]=\"row.typeSelect.tooltip\">\n <mat-select\n [panelWidth]=\"null\"\n [ngModel]=\"row.type\"\n (ngModelChange)=\"setType(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [disabled]=\"row.typeSelect.disabled\"\n aria-label=\"Type\">\n @for (option of typeOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n {{ typeLabel(row.type) }}\n }\n </td>\n <td class=\"filter-col-select\">\n <mat-form-field [subscriptSizing]=\"'dynamic'\">\n <mat-select\n [panelWidth]=\"null\"\n [ngModel]=\"row.placement\"\n (ngModelChange)=\"setPlacement(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n aria-label=\"Report bar\">\n @for (option of row.placementOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </td>\n @if (!isValue()) {\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setComponent(row, $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [disabled]=\"row.componentToggle.disabled\"\n [matTooltip]=\"row.componentToggle.tooltip\">\n </mat-slide-toggle>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <div class=\"filters-empty\">\n This component has no filterable columns yet \u2014 give it some SQL first.\n </div>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"SQL\"\n name=\"sql\">\n <div class=\"tab-body\">\n @if (sqlError()) {\n <div class=\"filters-empty error\">\n {{ sqlError() }}\n </div>\n } @else if (sql()) {\n @if (sqlBaseline()) {\n <!-- A derived component's statement is compiled from its\n document with no viewer state, so it is not byte-for-byte\n what produced the numbers on screen. Said here rather than\n left for the reader to discover. -->\n <div class=\"filters-empty\">\n Compiled from this component's query \u2014 no filters applied, times in {{ sqlTimezone() }}.\n </div>\n }\n <pre class=\"code-block\">{{ sql() }}</pre>\n } @else {\n <div class=\"filters-empty\">\n This component has no SQL yet.\n </div>\n }\n </div>\n </mat-tab>\n }\n <mat-tab\n label=\"Config\"\n name=\"config\">\n <div class=\"tab-body\">\n <pre class=\"code-block\">\n {{ configJson }}\n </pre>\n </div>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- Save | Delete | Cancel, the standard order. fs-form-dialog-actions\n renders its own Save/Done BEFORE the projected content, which would\n put Delete after Cancel, so `[save]=\"false\"` foots the row here and\n all three buttons are projected in order. Save submits the Settings\n tab; the other tabs persist as you touch them, so they get Done. -->\n <fs-form-dialog-actions [save]=\"false\">\n @if (selectedTab === 'settings') {\n <button\n mat-button\n type=\"submit\"\n color=\"primary\">\n Save\n </button>\n } @else {\n <button\n mat-button\n type=\"button\"\n color=\"primary\"\n [mat-dialog-close]=\"null\">\n Done\n </button>\n }\n <button\n mat-button\n type=\"button\"\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n <button\n mat-button\n type=\"button\"\n class=\"cancel-button\"\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}.geometry-row{display:flex;gap:8px}.geometry-row mat-form-field{flex:1}.filter-table{border-spacing:0;width:100%;border-collapse:collapse}.filter-table thead th{color:#8f8f8f;font-weight:400;font-size:85%;text-align:left;padding:8px 16px}.filter-table tbody tr{clip-path:xywh(0 3px 100% calc(100% - 6px) round 10px)}.filter-table tbody td{border:none;background-color:#fafafa;padding:8px 16px;vertical-align:middle;text-align:left}.filter-table .filter-col-name{font-weight:600;color:#1f2933}.filter-table .filter-col-type{color:#7b8794;width:150px}.filter-table .filter-col-select{width:160px}.filter-table td.filter-col-type,.filter-table td.filter-col-select{padding-left:8px;padding-right:8px}.filter-table td.filter-col-type mat-form-field,.filter-table td.filter-col-select mat-form-field{width:100%;--mat-form-field-container-height: 36px;--mat-form-field-container-vertical-padding: 6px}.filter-table td.filter-col-type ::ng-deep .mat-mdc-form-field-infix,.filter-table td.filter-col-select ::ng-deep .mat-mdc-form-field-infix{width:auto}.filter-table td.filter-col-type ::ng-deep .mat-mdc-form-field-subscript-wrapper,.filter-table td.filter-col-select ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.filter-table .filter-col-toggle,.filter-table th.filter-col-toggle{width:110px;text-align:center}.filters-empty{font-size:13px;color:#7b8794;padding:8px 0 4px}.filters-empty.error{color:#e15759}.code-block{margin:0;padding:10px 12px;background:#1f2933;color:#e4e7eb;border-radius:6px;font-family:JetBrains Mono,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto}\n"] }]
14329
14479
  }] });
14330
14480
 
14331
14481
  // Self-contained timezone picker for the report settings dialog. The IANA zone
@@ -14364,7 +14514,7 @@ class TimezoneSelectComponent {
14364
14514
  : zones;
14365
14515
  }
14366
14516
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TimezoneSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14367
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TimezoneSelectComponent, isStandalone: true, selector: "fs-ai-report-timezone-select", inputs: { placeholder: "placeholder", required: "required", disabled: "disabled", timezone: "timezone" }, outputs: { timezoneChange: "timezoneChange" }, ngImport: i0, template: "<mat-form-field class=\"full-width\">\n <mat-label>{{placeholder}}</mat-label>\n <mat-select\n [(ngModel)]=\"timezone\"\n [fsFormRequired]=\"required\"\n [disabled]=\"disabled\"\n name=\"timezone_select_{{ guid }}\">\n @for (item of timezones(); track item) {\n <mat-option [value]=\"item\">{{ item }}</mat-option>\n }\n </mat-select>\n</mat-form-field>\n", styles: [".full-width{width:100%}\n"], dependencies: [{ kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption$1, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { 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: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$2.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14517
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: TimezoneSelectComponent, isStandalone: true, selector: "fs-ai-report-timezone-select", inputs: { placeholder: "placeholder", required: "required", disabled: "disabled", timezone: "timezone" }, outputs: { timezoneChange: "timezoneChange" }, ngImport: i0, template: "<mat-form-field class=\"full-width\">\n <mat-label>{{placeholder}}</mat-label>\n <mat-select\n [(ngModel)]=\"timezone\"\n [fsFormRequired]=\"required\"\n [disabled]=\"disabled\"\n name=\"timezone_select_{{ guid }}\">\n @for (item of timezones(); track item) {\n <mat-option [value]=\"item\">{{ item }}</mat-option>\n }\n </mat-select>\n</mat-form-field>\n", styles: [".full-width{width:100%}\n"], dependencies: [{ kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption$1, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { 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: "ngmodule", type: FsFormModule }, { kind: "directive", type: i3.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14368
14518
  }
14369
14519
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TimezoneSelectComponent, decorators: [{
14370
14520
  type: Component,
@@ -14846,7 +14996,7 @@ class ReportSettingsComponent {
14846
14996
  }
14847
14997
  }
14848
14998
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14849
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <!-- One skeleton over the whole body until the locale list is in. -->\n <ng-container *fsSkeletonForm=\"ready() || null\">\n <!-- animationDone: a refused Label or Name is focused once the\n Filters tab is on screen. -->\n <mat-tab-group\n [(selected)]=\"selectedTab\"\n (animationDone)=\"focusRefusal()\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm.align-start\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n </div>\n <!--\n How deep the header and footer bands are. Their content is edited\n on the canvas (Edit layout \u2192 Header & footer); these reserve the\n space for it, and 0 means no band.\n -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Header height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"headerHeight\"\n name=\"headerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Footer height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"footerHeight\"\n name=\"footerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n <!--\n How numbers and dates are written (formatting only: no text is\n translated) and which currency money is labeled in. Hidden on a\n server too old to resolve a report's locale.\n -->\n @if (locales()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Locale\n </mat-label>\n <mat-select\n [ngModel]=\"locale()\"\n (ngModelChange)=\"locale.set($event)\"\n name=\"locale\">\n @for (option of localeOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n {{ formatExample() }}\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Currency\n </mat-label>\n <mat-select\n [ngModel]=\"currency()\"\n (ngModelChange)=\"currency.set($event)\"\n name=\"currency\">\n @for (option of currencyOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Names the currency amounts are in; it does not convert them.\n </mat-hint>\n </mat-form-field>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n <!-- One section per report bar filter: its heading, its label\n and name, then its default, one field per row. -->\n @for (row of filterRows(); track row.group.id) {\n <h3>\n {{ row.heading }}\n </h3>\n <!-- The label and the name are independent: a new label\n never changes the name, and the name is edited on its\n own. The server checks both, and its refusal shows on\n the field (refusalValidator). -->\n @if (row.showName) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Label\n </mat-label>\n <input\n matInput\n [id]=\"labelInputId(row.group.id)\"\n [ngModel]=\"row.label\"\n (ngModelChange)=\"patchRow(row, { label: $event })\"\n [name]=\"'label' + row.group.id\"\n [fsFormRequired]=\"true\"\n [fsFormMaxLength]=\"labelMaxLength\"\n [validate]=\"refusalValidator\"\n [validateData]=\"row.labelRefusal\">\n <mat-hint>\n What the filter shows. Changing it never changes the name.\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [id]=\"nameInputId(row.group.id)\"\n [ngModel]=\"row.name\"\n (ngModelChange)=\"patchRow(row, { name: $event })\"\n [name]=\"'name' + row.group.id\"\n [fsFormRequired]=\"true\"\n [fsFormPattern]=\"namePattern\"\n [fsFormPatternMessage]=\"namePatternMessage\"\n [fsFormMaxLength]=\"nameMaxLength\"\n [validate]=\"refusalValidator\"\n [validateData]=\"row.nameRefusal\">\n <mat-hint>\n How the AI refers to this filter: a letter, then letters and digits.\n </mat-hint>\n </mat-form-field>\n }\n @switch (row.group.type) {\n @case ('dateRange') {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default period\n </mat-label>\n <mat-select\n [ngModel]=\"row.period\"\n (ngModelChange)=\"patchRow(row, { period: $event })\"\n [name]=\"'period' + row.group.id\">\n @for (option of row.periodOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (row.period === fixedDatesPeriod) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default from\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default to\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n }\n }\n @case ('select') {\n <fs-label-field class=\"full-width\">\n <fs-autocomplete-chips\n class=\"full-width\"\n label=\"Default values\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n </fs-label-field>\n @if (row.showExclude) {\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"row.exclude\"\n (ngModelChange)=\"patchRow(row, { exclude: $event })\"\n [name]=\"'exclude' + row.group.id\">\n Exclude these values\n </mat-slide-toggle>\n </fs-label-field>\n }\n }\n @case ('numberRange') {\n <!-- fsMaskNumber hands back text; the check and the save\n read it through parseDefaultBound, so 0 is kept. -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default minimum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.min\"\n (ngModelChange)=\"patchRow(row, { min: $event })\"\n [name]=\"'min' + row.group.id\"\n [validate]=\"minBoundValidator\"\n [validateData]=\"row.max\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default maximum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.max\"\n (ngModelChange)=\"patchRow(row, { max: $event })\"\n [name]=\"'max' + row.group.id\"\n [validate]=\"maxBoundValidator\"\n [validateData]=\"row.min\">\n </mat-form-field>\n }\n @default {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default text\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report bar filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$2.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i2$2.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$2.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$2.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "directive", type: i2$2.FsFormMaxLengthDirective, selector: "[fsFormMaxLength]", inputs: ["fsFormMaxLength", "fsFormMaxLengthMessage"] }, { kind: "directive", type: i2$2.FsFormPatternDirective, selector: "[fsFormPattern]", inputs: ["fsFormPattern", "fsFormPatternMessage"] }, { kind: "directive", type: i2$2.FsFormValidateDirective, selector: "[validate]", inputs: ["validate", "validateData", "validateOnSubmit"] }, { kind: "component", type: i2$2.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$2.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i2$2.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "component", type: i5$1.FsLabelFieldComponent, selector: "fs-label-field", inputs: ["appearance", "showOutline", "disabled", "focused", "hoverable", "padless"] }, { kind: "ngmodule", type: FsMaskModule }, { kind: "directive", type: i3$1.FsMaskNumberDirective, selector: "[fsMaskNumber]" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonFormDirective, selector: "[fsSkeletonForm]", inputs: ["fsSkeletonForm", "fsSkeletonFormLines"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i9.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i10.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i10.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i6.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i6.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14999
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <!-- One skeleton over the whole body until the locale list is in. -->\n <ng-container *fsSkeletonForm=\"ready() || null\">\n <!-- animationDone: a refused Label or Name is focused once the\n Filters tab is on screen. -->\n <mat-tab-group\n [(selected)]=\"selectedTab\"\n (animationDone)=\"focusRefusal()\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm.align-start\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n </div>\n <!--\n How deep the header and footer bands are. Their content is edited\n on the canvas (Edit layout \u2192 Header & footer); these reserve the\n space for it, and 0 means no band.\n -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Header height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"headerHeight\"\n name=\"headerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Footer height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"footerHeight\"\n name=\"footerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n <!--\n How numbers and dates are written (formatting only: no text is\n translated) and which currency money is labeled in. Hidden on a\n server too old to resolve a report's locale.\n -->\n @if (locales()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Locale\n </mat-label>\n <mat-select\n [ngModel]=\"locale()\"\n (ngModelChange)=\"locale.set($event)\"\n name=\"locale\">\n @for (option of localeOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n {{ formatExample() }}\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Currency\n </mat-label>\n <mat-select\n [ngModel]=\"currency()\"\n (ngModelChange)=\"currency.set($event)\"\n name=\"currency\">\n @for (option of currencyOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Names the currency amounts are in; it does not convert them.\n </mat-hint>\n </mat-form-field>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n <!-- One section per report bar filter: its heading, its label\n and name, then its default, one field per row. -->\n @for (row of filterRows(); track row.group.id) {\n <h3>\n {{ row.heading }}\n </h3>\n <!-- The label and the name are independent: a new label\n never changes the name, and the name is edited on its\n own. The server checks both, and its refusal shows on\n the field (refusalValidator). -->\n @if (row.showName) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Label\n </mat-label>\n <input\n matInput\n [id]=\"labelInputId(row.group.id)\"\n [ngModel]=\"row.label\"\n (ngModelChange)=\"patchRow(row, { label: $event })\"\n [name]=\"'label' + row.group.id\"\n [fsFormRequired]=\"true\"\n [fsFormMaxLength]=\"labelMaxLength\"\n [validate]=\"refusalValidator\"\n [validateData]=\"row.labelRefusal\">\n <mat-hint>\n What the filter shows. Changing it never changes the name.\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [id]=\"nameInputId(row.group.id)\"\n [ngModel]=\"row.name\"\n (ngModelChange)=\"patchRow(row, { name: $event })\"\n [name]=\"'name' + row.group.id\"\n [fsFormRequired]=\"true\"\n [fsFormPattern]=\"namePattern\"\n [fsFormPatternMessage]=\"namePatternMessage\"\n [fsFormMaxLength]=\"nameMaxLength\"\n [validate]=\"refusalValidator\"\n [validateData]=\"row.nameRefusal\">\n <mat-hint>\n How the AI refers to this filter: a letter, then letters and digits.\n </mat-hint>\n </mat-form-field>\n }\n @switch (row.group.type) {\n @case ('dateRange') {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default period\n </mat-label>\n <mat-select\n [ngModel]=\"row.period\"\n (ngModelChange)=\"patchRow(row, { period: $event })\"\n [name]=\"'period' + row.group.id\">\n @for (option of row.periodOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (row.period === fixedDatesPeriod) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default from\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default to\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n }\n }\n @case ('select') {\n <fs-label-field class=\"full-width\">\n <fs-autocomplete-chips\n class=\"full-width\"\n label=\"Default values\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n </fs-label-field>\n @if (row.showExclude) {\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"row.exclude\"\n (ngModelChange)=\"patchRow(row, { exclude: $event })\"\n [name]=\"'exclude' + row.group.id\">\n Exclude these values\n </mat-slide-toggle>\n </fs-label-field>\n }\n }\n @case ('numberRange') {\n <!-- fsMaskNumber hands back text; the check and the save\n read it through parseDefaultBound, so 0 is kept. -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default minimum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.min\"\n (ngModelChange)=\"patchRow(row, { min: $event })\"\n [name]=\"'min' + row.group.id\"\n [validate]=\"minBoundValidator\"\n [validateData]=\"row.max\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default maximum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.max\"\n (ngModelChange)=\"patchRow(row, { max: $event })\"\n [name]=\"'max' + row.group.id\"\n [validate]=\"maxBoundValidator\"\n [validateData]=\"row.min\">\n </mat-form-field>\n }\n @default {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default text\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report bar filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i3.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i3.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i3.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i3.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "directive", type: i3.FsFormMaxLengthDirective, selector: "[fsFormMaxLength]", inputs: ["fsFormMaxLength", "fsFormMaxLengthMessage"] }, { kind: "directive", type: i3.FsFormPatternDirective, selector: "[fsFormPattern]", inputs: ["fsFormPattern", "fsFormPatternMessage"] }, { kind: "directive", type: i3.FsFormValidateDirective, selector: "[validate]", inputs: ["validate", "validateData", "validateOnSubmit"] }, { kind: "component", type: i3.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i3.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i3.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "component", type: i6$1.FsLabelFieldComponent, selector: "fs-label-field", inputs: ["appearance", "showOutline", "disabled", "focused", "hoverable", "padless"] }, { kind: "ngmodule", type: FsMaskModule }, { kind: "directive", type: i4$3.FsMaskNumberDirective, selector: "[fsMaskNumber]" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i2$1.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i7.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i7.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonFormDirective, selector: "[fsSkeletonForm]", inputs: ["fsSkeletonForm", "fsSkeletonFormLines"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i2$4.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i10.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i10.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i6.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i6.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14850
15000
  }
14851
15001
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, decorators: [{
14852
15002
  type: Component,
@@ -15617,7 +15767,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
15617
15767
  // in the report's format context, as on screen.
15618
15768
  async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
15619
15769
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
15620
- const echarts = (await import('./firestitch-report-echarts-DrTDdBIP.mjs')).default;
15770
+ const echarts = (await import('./firestitch-report-echarts-WOx5HoDM.mjs')).default;
15621
15771
  const host = document.createElement('div');
15622
15772
  host.style.width = `${width}px`;
15623
15773
  host.style.height = `${height}px`;
@@ -19282,11 +19432,11 @@ class FsAiReportComponent {
19282
19432
  ReportAiChatData,
19283
19433
  { provide: FsAiChatData, useExisting: ReportAiChatData },
19284
19434
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
19285
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DrTDdBIP.mjs').then((module) => module.default) }),
19435
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-WOx5HoDM.mjs').then((module) => module.default) }),
19286
19436
  // The bar's date pickers start their weeks on Monday and list the built-in
19287
19437
  // presets; a preset only sets the days.
19288
19438
  provideReportDatePicker(),
19289
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }, { propertyName: "_reportBar", first: true, predicate: FilterComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report()?.name }}</h2>\n\n @if (report()) {\n <!-- Report-level filters, inline with the toolbar actions, with the\n report's saved filters in fs-filter's own Saved filters menu.\n fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the\n block is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportData(false, 'xlsx')\">\n Export Excel\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true, 'xlsx')\">\n Export Excel (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportPackage()\">\n Export Report Package\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n @if (chatEnabled()) {\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"chatBasePath()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n }\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$3.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$4.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$4.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$4.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19439
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }, { propertyName: "_reportBar", first: true, predicate: FilterComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report()?.name }}</h2>\n\n @if (report()) {\n <!-- Report-level filters, inline with the toolbar actions, with the\n report's saved filters in fs-filter's own Saved filters menu.\n fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the\n block is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportData(false, 'xlsx')\">\n Export Excel\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true, 'xlsx')\">\n Export Excel (This Page)\n </ng-template>\n }\n <ng-template\n fs-menu-item\n (click)=\"exportPackage()\">\n Export Report Package\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n @if (chatEnabled()) {\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"chatBasePath()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n }\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$3.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19290
19440
  }
19291
19441
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
19292
19442
  type: Component,
@@ -19306,7 +19456,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19306
19456
  ReportAiChatData,
19307
19457
  { provide: FsAiChatData, useExisting: ReportAiChatData },
19308
19458
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
19309
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DrTDdBIP.mjs').then((module) => module.default) }),
19459
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-WOx5HoDM.mjs').then((module) => module.default) }),
19310
19460
  // The bar's date pickers start their weeks on Monday and list the built-in
19311
19461
  // presets; a preset only sets the days.
19312
19462
  provideReportDatePicker(),
@@ -19398,5 +19548,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19398
19548
  * Generated bundle index. Do not edit.
19399
19549
  */
19400
19550
 
19401
- 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 };
19402
- //# sourceMappingURL=firestitch-report-firestitch-report-Bgr_IprU.mjs.map
19551
+ export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, REPORT_CHART_FONT_FAMILY as a, REPORT_AXIS_LABEL_FONT_SIZE as b, FsAiReportComponent as c, ReportData as d, ReportService as e, ReportFilterStateService as f, ReportNodeType as g, ReportState as h, ReportValueFormat as i, ReportValueFormula as j, ReportListColumnFormat as k, ReportNumberUnits as l, ReportChartDataLabels as m, ReportChartColor as n, ReportChartLineStyle as o, ReportChartMarkStat as p, ReportChartDateLabel as q, ReportChartSpanThrough as r, ReportChangePolarity as s };
19552
+ //# sourceMappingURL=firestitch-report-firestitch-report-DztXL6p8.mjs.map