@firestitch/report 18.0.2 → 18.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/reports/data/report.data.d.ts +5 -2
- package/app/reports/dialogs/report-settings/report-settings-default.d.ts +2 -0
- package/app/reports/dialogs/report-settings/report-settings.component.d.ts +24 -1
- package/app/reports/interfaces/report.interface.d.ts +3 -0
- package/app/reports/report-filter-items.d.ts +12 -0
- package/app/reports/views/report/fs-ai-report.component.d.ts +43 -0
- package/app/reports/views/reports/fs-ai-reports.component.d.ts +23 -0
- package/esm2022/app/reports/data/report.data.mjs +7 -4
- package/esm2022/app/reports/dialogs/component-settings/component-settings.component.mjs +4 -4
- package/esm2022/app/reports/dialogs/report-settings/report-settings-default.mjs +22 -0
- package/esm2022/app/reports/dialogs/report-settings/report-settings.component.mjs +114 -15
- package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
- package/esm2022/app/reports/report-filter-items.mjs +28 -28
- package/esm2022/app/reports/services/report-filter-state.service.mjs +11 -3
- package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +312 -0
- package/esm2022/app/reports/views/reports/fs-ai-reports.component.mjs +112 -0
- package/esm2022/public_api.mjs +5 -1
- package/fesm2022/{firestitch-report-echarts-Bgshq3H7.mjs → firestitch-report-echarts-m2yVum4J.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-Bgshq3H7.mjs.map → firestitch-report-echarts-m2yVum4J.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-DJ3o7KId.mjs → firestitch-report-firestitch-report-BclPOljK.mjs} +576 -60
- package/fesm2022/firestitch-report-firestitch-report-BclPOljK.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/public_api.d.ts +2 -0
- package/fesm2022/firestitch-report-firestitch-report-DJ3o7KId.mjs.map +0 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, Component, ChangeDetectionStrategy, Input, DestroyRef, ViewChild, EventEmitter, ChangeDetectorRef, NgZone, ElementRef, Output, HostBinding, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
2
|
+
import { inject, Injectable, Component, ChangeDetectionStrategy, Input, DestroyRef, ViewChild, EventEmitter, ChangeDetectorRef, NgZone, ElementRef, Output, HostBinding, signal, CUSTOM_ELEMENTS_SCHEMA, input, output, effect } from '@angular/core';
|
|
3
3
|
import * as i1$3 from '@angular/forms';
|
|
4
4
|
import { FormsModule, ControlContainer, NgForm } from '@angular/forms';
|
|
5
5
|
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialog } from '@angular/material/dialog';
|
|
6
|
-
import
|
|
6
|
+
import * as i4$2 from '@angular/material/icon';
|
|
7
|
+
import { MatIcon, MatIconModule } from '@angular/material/icon';
|
|
7
8
|
import * as i2$2 from '@firestitch/autocomplete-chips';
|
|
8
9
|
import { FsAutocompleteChipsModule } from '@firestitch/autocomplete-chips';
|
|
9
10
|
import * as i2 from '@firestitch/filter';
|
|
10
11
|
import { ItemType, FsFilterModule } from '@firestitch/filter';
|
|
11
|
-
import * as
|
|
12
|
+
import * as i2$3 from '@firestitch/menu';
|
|
12
13
|
import { FsMenuModule } from '@firestitch/menu';
|
|
13
14
|
import { FsProcess } from '@firestitch/process';
|
|
14
15
|
import { FsPrompt } from '@firestitch/prompt';
|
|
@@ -17,8 +18,10 @@ import { map, shareReplay, filter, tap, switchMap, catchError, finalize } from '
|
|
|
17
18
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
18
19
|
import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
|
|
19
20
|
import { FsAiChatComponent } from '@firestitch/ai';
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
21
|
+
import * as i3$1 from '@angular/material/button';
|
|
22
|
+
import { MatIconButton, MatButton, MatButtonModule } from '@angular/material/button';
|
|
23
|
+
import * as i5$1 from '@angular/material/tooltip';
|
|
24
|
+
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
|
|
22
25
|
import * as i1$2 from '@firestitch/zoom-pan';
|
|
23
26
|
import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
|
|
24
27
|
import { FsApi } from '@firestitch/api';
|
|
@@ -38,12 +41,18 @@ import { FsDialogModule } from '@firestitch/dialog';
|
|
|
38
41
|
import * as i2$1 from '@firestitch/form';
|
|
39
42
|
import { FsFormModule } from '@firestitch/form';
|
|
40
43
|
import { FsLabelModule } from '@firestitch/label';
|
|
41
|
-
import
|
|
44
|
+
import * as i4$1 from '@firestitch/message';
|
|
45
|
+
import { FsMessage, FsMessageModule } from '@firestitch/message';
|
|
42
46
|
import * as i4 from '@firestitch/tabs';
|
|
43
47
|
import { FsTabsModule } from '@firestitch/tabs';
|
|
44
48
|
import { MatSelect, MatOption as MatOption$1 } from '@angular/material/select';
|
|
49
|
+
import * as i6 from '@firestitch/datepicker';
|
|
50
|
+
import { FsDatePickerModule } from '@firestitch/datepicker';
|
|
45
51
|
import { MatOption } from '@angular/material/core';
|
|
46
52
|
import { guid } from '@firestitch/common';
|
|
53
|
+
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
|
54
|
+
import * as i1$4 from '@firestitch/date';
|
|
55
|
+
import { FsDateModule } from '@firestitch/date';
|
|
47
56
|
|
|
48
57
|
// API gateway for the Reports module. Every endpoint hangs off [basePath],
|
|
49
58
|
// which defaults to "reports" (the framework reports backend at /api/reports)
|
|
@@ -177,9 +186,12 @@ class ReportData {
|
|
|
177
186
|
...config,
|
|
178
187
|
});
|
|
179
188
|
}
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
// Update a filter group — its exposure level ('component' | 'report' | 'both')
|
|
190
|
+
// and/or its default value (the value seeding the report bar on open). Send
|
|
191
|
+
// `default: null` (or an empty default) to clear it. Only the keys provided
|
|
192
|
+
// are changed.
|
|
193
|
+
updateFilterGroup(reportId, groupId, changes, config = {}) {
|
|
194
|
+
return this._api.put(this._path(`${reportId}/filtergroups/${groupId}`), changes, {
|
|
183
195
|
key: 'filterGroup',
|
|
184
196
|
...config,
|
|
185
197
|
});
|
|
@@ -265,6 +277,30 @@ function dateBoundString(value) {
|
|
|
265
277
|
// the group-keyed path for charts/KPIs and the report bar.)
|
|
266
278
|
// The query key fs-filter uses for a group's item(s).
|
|
267
279
|
const itemName = (group) => `g${group.id}`;
|
|
280
|
+
// A select group's distinct options, as { name, value } pairs. Options come from
|
|
281
|
+
// the first member filter that declares them — members are the same data point,
|
|
282
|
+
// so any member's list serves the group. The list is fetched once (the server
|
|
283
|
+
// caps it) and replayed (shareReplay), so repeated reads never refetch. Shared
|
|
284
|
+
// by the report-bar select control and the report-settings default picker so the
|
|
285
|
+
// two never drift. Returns an empty list when no member provides options.
|
|
286
|
+
function loadGroupOptions(group, reportData, reportId) {
|
|
287
|
+
const optionFilter = (group.filters ?? []).find((member) => member.hasOptions);
|
|
288
|
+
if (!optionFilter) {
|
|
289
|
+
return of([]);
|
|
290
|
+
}
|
|
291
|
+
return reportData.filterOptions(reportId, optionFilter.id)
|
|
292
|
+
.pipe(map((options) => (options ?? [])
|
|
293
|
+
.map((option) => ({ name: String(option), value: option }))), shareReplay({ bufferSize: 1, refCount: false }));
|
|
294
|
+
}
|
|
295
|
+
// Case-insensitive name filter applied to a loaded option list — the typed
|
|
296
|
+
// keyword has to be matched here because fs-autocomplete renders what we return
|
|
297
|
+
// verbatim.
|
|
298
|
+
function matchOptions(options$, keyword) {
|
|
299
|
+
const term = (keyword ?? '').trim().toLowerCase();
|
|
300
|
+
return options$.pipe(map((options) => term
|
|
301
|
+
? options.filter((option) => option.name.toLowerCase().includes(term))
|
|
302
|
+
: options));
|
|
303
|
+
}
|
|
268
304
|
// Build the fs-filter config item for a group, seeding its default from the
|
|
269
305
|
// current session value so authored/relative defaults show selected on open.
|
|
270
306
|
function filterItemForGroup(group, reportData, reportId, initial) {
|
|
@@ -281,38 +317,14 @@ function filterItemForGroup(group, reportData, reportId, initial) {
|
|
|
281
317
|
: undefined,
|
|
282
318
|
};
|
|
283
319
|
case 'select': {
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
const optionFilter = (group.filters ?? []).find((member) => member.hasOptions);
|
|
287
|
-
// The distinct option list is fetched once (server caps it) and replayed
|
|
288
|
-
// to every keystroke, so typing never refetches.
|
|
289
|
-
let options$ = null;
|
|
290
|
-
const loadOptions = () => {
|
|
291
|
-
if (!optionFilter) {
|
|
292
|
-
return of([]);
|
|
293
|
-
}
|
|
294
|
-
if (!options$) {
|
|
295
|
-
options$ = reportData.filterOptions(reportId, optionFilter.id)
|
|
296
|
-
.pipe(map((options) => (options ?? [])
|
|
297
|
-
.map((option) => ({ name: String(option), value: option }))), shareReplay({ bufferSize: 1, refCount: false }));
|
|
298
|
-
}
|
|
299
|
-
return options$;
|
|
300
|
-
};
|
|
320
|
+
// One shared, replayed option stream so typing never refetches.
|
|
321
|
+
const options$ = loadGroupOptions(group, reportData, reportId);
|
|
301
322
|
return {
|
|
302
323
|
name,
|
|
303
324
|
type: ItemType.AutoCompleteChips,
|
|
304
325
|
label,
|
|
305
326
|
fetchOnFocus: true,
|
|
306
|
-
|
|
307
|
-
// keyword has to be matched here. Match case-insensitively against the
|
|
308
|
-
// displayed name — works for any select filter regardless of its column.
|
|
309
|
-
values: (keyword) => loadOptions()
|
|
310
|
-
.pipe(map((options) => {
|
|
311
|
-
const term = (keyword ?? '').trim().toLowerCase();
|
|
312
|
-
return term
|
|
313
|
-
? options.filter((option) => option.name.toLowerCase().includes(term))
|
|
314
|
-
: options;
|
|
315
|
-
})),
|
|
327
|
+
values: (keyword) => matchOptions(options$, keyword),
|
|
316
328
|
default: initial?.values?.length
|
|
317
329
|
? initial.values.map((value) => ({ name: String(value), value }))
|
|
318
330
|
: undefined,
|
|
@@ -486,8 +498,10 @@ class ReportFilterStateService {
|
|
|
486
498
|
}
|
|
487
499
|
return resolved;
|
|
488
500
|
}
|
|
489
|
-
// A group's authored default
|
|
490
|
-
// time the report opens, so a saved
|
|
501
|
+
// A group's authored default, mapped to a session value by type. Relative
|
|
502
|
+
// date presets resolve against "now" each time the report opens, so a saved
|
|
503
|
+
// "last 12 months" stays relative; a fixed range, select values and keyword
|
|
504
|
+
// text seed verbatim.
|
|
491
505
|
_defaultValue(group) {
|
|
492
506
|
const groupDefault = group.config?.default;
|
|
493
507
|
if (!groupDefault) {
|
|
@@ -503,6 +517,12 @@ class ReportFilterStateService {
|
|
|
503
517
|
end: groupDefault.end ?? null,
|
|
504
518
|
};
|
|
505
519
|
}
|
|
520
|
+
if (groupDefault.values?.length) {
|
|
521
|
+
return { values: groupDefault.values };
|
|
522
|
+
}
|
|
523
|
+
if (groupDefault.value) {
|
|
524
|
+
return { value: groupDefault.value };
|
|
525
|
+
}
|
|
506
526
|
return null;
|
|
507
527
|
}
|
|
508
528
|
_relativeRange(relative) {
|
|
@@ -1979,7 +1999,7 @@ class ComponentSettingsComponent {
|
|
|
1979
1999
|
? 'both'
|
|
1980
2000
|
: (row.report ? 'report' : 'component');
|
|
1981
2001
|
if (existing) {
|
|
1982
|
-
this._run(this._reportData.
|
|
2002
|
+
this._run(this._reportData.updateFilterGroup(this.report.id, existing.filterGroupId, { level }));
|
|
1983
2003
|
}
|
|
1984
2004
|
else {
|
|
1985
2005
|
this._run(this._reportData.addFilter(this.report.id, this.component.id, {
|
|
@@ -2059,7 +2079,7 @@ class ComponentSettingsComponent {
|
|
|
2059
2079
|
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
2060
2080
|
}
|
|
2061
2081
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2062
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ component.title || 'Component' }}\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 <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"title\"\n name=\"title\">\n </mat-form-field>\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 type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\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. Toggle where its control shows: in\n the report bar, on this component, both, or neither. The\n interface (date range vs value picker) follows the data. -->\n <div class=\"filter-list\">\n <div class=\"filter-head\">\n <span class=\"filter-head-spacer\"></span>\n <span class=\"small\">\n Report bar\n </span>\n <span class=\"small\">\n This component\n </span>\n </div>\n @for (row of rows; track row.column) {\n <div class=\"filter-card\">\n <div class=\"filter-card-main\">\n <div class=\"filter-card-title\">\n {{ row.label }}\n </div>\n <div class=\"filter-card-meta\">\n {{ typeLabel(row.type) }}\n </div>\n </div>\n <mat-slide-toggle\n class=\"filter-toggle\"\n [ngModel]=\"row.report\"\n (ngModelChange)=\"setExposure(row, 'report', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show in the report bar\">\n </mat-slide-toggle>\n <mat-slide-toggle\n class=\"filter-toggle\"\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setExposure(row, 'component', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show on this component\">\n </mat-slide-toggle>\n </div>\n }\n </div>\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 <pre class=\"code-block\">\n {{ component.sql }}\n </pre>\n </div>\n </mat-tab>\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 <fs-form-dialog-actions\n [save]=\"selectedTab === 'settings'\"\n [done]=\"selectedTab !== 'settings'\">\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-list{display:flex;flex-direction:column;gap:8px}.filter-head{display:flex;align-items:center;gap:8px;padding:0 10px 2px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:#9aa5b1}.filter-head .filter-head-spacer{flex:1}.filter-head .filter-head-toggle{width:110px;flex:0 0 auto;text-align:center}.filter-card{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid #e4e7eb;border-radius:8px;background:#fff}.filter-card .filter-card-main{flex:1;min-width:0}.filter-card .filter-card-main .filter-card-title{font-weight:600;font-size:13px;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.filter-card .filter-card-main .filter-card-meta{font-size:12px;color:#7b8794}.filter-card .filter-card-main .filter-card-meta code{background:#f0f4f8;border-radius:3px;padding:0 4px}.filter-card .filter-toggle{width:110px;flex:0 0 auto;display:flex;justify-content: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$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.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$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.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: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i4.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i4.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5.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: "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: 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: 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2082
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ component.title || 'Component' }}\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 <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"title\"\n name=\"title\">\n </mat-form-field>\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 type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\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. Toggle where its control shows: in\n the report bar, on this component, both, or neither. The\n interface (date range vs value picker) follows the data. -->\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-toggle\">\n Report bar\n </th>\n <th class=\"filter-col-toggle\">\n This component\n </th>\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 {{ typeLabel(row.type) }}\n </td>\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.report\"\n (ngModelChange)=\"setExposure(row, 'report', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show in the report bar\">\n </mat-slide-toggle>\n </td>\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setExposure(row, 'component', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show on this component\">\n </mat-slide-toggle>\n </td>\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 <pre class=\"code-block\">\n {{ component.sql }}\n </pre>\n </div>\n </mat-tab>\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 <fs-form-dialog-actions\n [save]=\"selectedTab === 'settings'\"\n [done]=\"selectedTab !== 'settings'\">\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:120px}.filter-table .filter-col-toggle,.filter-table th.filter-col-toggle{width:130px;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$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.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$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.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: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i4.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i4.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5.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: "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: 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: 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2063
2083
|
}
|
|
2064
2084
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentSettingsComponent, decorators: [{
|
|
2065
2085
|
type: Component,
|
|
@@ -2079,7 +2099,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2079
2099
|
MatInput,
|
|
2080
2100
|
MatSlideToggle,
|
|
2081
2101
|
MatTooltip,
|
|
2082
|
-
], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ component.title || 'Component' }}\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 <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"title\"\n name=\"title\">\n </mat-form-field>\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 type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\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. Toggle where its control shows: in\n the report bar, on this component, both, or neither. The\n interface (date range vs value picker) follows the data. -->\n <
|
|
2102
|
+
], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n {{ component.title || 'Component' }}\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 <mat-form-field>\n <mat-label>\n Title\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"title\"\n name=\"title\">\n </mat-form-field>\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 type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingTop\"\n name=\"paddingTop\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Right\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingRight\"\n name=\"paddingRight\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Bottom\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingBottom\"\n name=\"paddingBottom\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Left\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.05\"\n min=\"0\"\n [(ngModel)]=\"paddingLeft\"\n name=\"paddingLeft\">\n <span matTextSuffix>\n in\n </span>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\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. Toggle where its control shows: in\n the report bar, on this component, both, or neither. The\n interface (date range vs value picker) follows the data. -->\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-toggle\">\n Report bar\n </th>\n <th class=\"filter-col-toggle\">\n This component\n </th>\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 {{ typeLabel(row.type) }}\n </td>\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.report\"\n (ngModelChange)=\"setExposure(row, 'report', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show in the report bar\">\n </mat-slide-toggle>\n </td>\n <td class=\"filter-col-toggle\">\n <mat-slide-toggle\n [ngModel]=\"row.component\"\n (ngModelChange)=\"setExposure(row, 'component', $event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n matTooltip=\"Show on this component\">\n </mat-slide-toggle>\n </td>\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 <pre class=\"code-block\">\n {{ component.sql }}\n </pre>\n </div>\n </mat-tab>\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 <fs-form-dialog-actions\n [save]=\"selectedTab === 'settings'\"\n [done]=\"selectedTab !== 'settings'\">\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:120px}.filter-table .filter-col-toggle,.filter-table th.filter-col-toggle{width:130px;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"] }]
|
|
2083
2103
|
}] });
|
|
2084
2104
|
|
|
2085
2105
|
// Self-contained timezone picker for the report settings dialog. The IANA zone
|
|
@@ -2142,12 +2162,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2142
2162
|
type: Input
|
|
2143
2163
|
}] } });
|
|
2144
2164
|
|
|
2145
|
-
//
|
|
2146
|
-
//
|
|
2147
|
-
//
|
|
2148
|
-
//
|
|
2149
|
-
//
|
|
2150
|
-
|
|
2165
|
+
// Date-range defaults are stored as calendar-day strings (yyyy-MM-dd, no time,
|
|
2166
|
+
// no timezone) so the backend never shifts them; the date picker works in Date
|
|
2167
|
+
// objects. These two convert between the two forms — the only translation the
|
|
2168
|
+
// Filters tab needs for a fixed date range.
|
|
2169
|
+
// A stored yyyy-MM-dd boundary as a local Date for the picker, or null.
|
|
2170
|
+
function dateFromBound(value) {
|
|
2171
|
+
if (!value) {
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
const date = parseISO(value.slice(0, 10));
|
|
2175
|
+
return isNaN(date.getTime()) ? null : date;
|
|
2176
|
+
}
|
|
2177
|
+
// A picked Date as a yyyy-MM-dd boundary for storage, or null. Uses the local
|
|
2178
|
+
// calendar day (format, not toISOString) so the boundary is the day the admin
|
|
2179
|
+
// picked, regardless of timezone.
|
|
2180
|
+
function dateToBound(value) {
|
|
2181
|
+
return value instanceof Date && !isNaN(value.getTime())
|
|
2182
|
+
? format(value, 'yyyy-MM-dd')
|
|
2183
|
+
: null;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// Report settings, in three tabs:
|
|
2187
|
+
// Settings — name + page setup (size, orientation, layout mode).
|
|
2188
|
+
// Styles — the report's typographic styles (sizes in POINTS, 1:1 with
|
|
2189
|
+
// PDF/PowerPoint); the Heading size to start.
|
|
2190
|
+
// Filters — the report-level filters and their DEFAULT values. A default
|
|
2191
|
+
// seeds the report bar on every open (for every viewer) but stays
|
|
2192
|
+
// user-changeable — it pre-fills the control, it doesn't lock it. This is the
|
|
2193
|
+
// fix for heavy reports opening blank and timing out: a default month +
|
|
2194
|
+
// organization scopes the landing page from the first load.
|
|
2151
2195
|
class ReportSettingsComponent {
|
|
2152
2196
|
selectedTab = 'settings';
|
|
2153
2197
|
name = '';
|
|
@@ -2158,6 +2202,8 @@ class ReportSettingsComponent {
|
|
|
2158
2202
|
// viewer's browser zone (which Save persists, making the report's zone explicit).
|
|
2159
2203
|
timezone = '';
|
|
2160
2204
|
headingSize = DEFAULT_HEADING_SIZE;
|
|
2205
|
+
// The report-level filters and their editable default values (Filters tab).
|
|
2206
|
+
filterRows = signal([]);
|
|
2161
2207
|
_data = inject(MAT_DIALOG_DATA);
|
|
2162
2208
|
_dialogRef = inject(MatDialogRef);
|
|
2163
2209
|
_reportData = inject(ReportData);
|
|
@@ -2172,6 +2218,7 @@ class ReportSettingsComponent {
|
|
|
2172
2218
|
this.layout = report.layout ?? 'freeform';
|
|
2173
2219
|
this.timezone = report.timezone ?? '';
|
|
2174
2220
|
this.headingSize = report.config?.styles?.heading?.size ?? DEFAULT_HEADING_SIZE;
|
|
2221
|
+
this._buildFilterRows(report);
|
|
2175
2222
|
}
|
|
2176
2223
|
// Confirm and delete here, while the dialog is still open, then close with
|
|
2177
2224
|
// a `deleted` result. The page reacts to the result; it no longer owns the
|
|
@@ -2188,21 +2235,103 @@ class ReportSettingsComponent {
|
|
|
2188
2235
|
});
|
|
2189
2236
|
}
|
|
2190
2237
|
save = () => {
|
|
2191
|
-
|
|
2238
|
+
const reportId = this._data.report.id;
|
|
2239
|
+
// Persist the report settings and each changed filter default together, so
|
|
2240
|
+
// the single Save covers all three tabs. The report reloads on the `saved`
|
|
2241
|
+
// result, re-seeding the bar from the new defaults.
|
|
2242
|
+
const settings$ = this._reportData.update(reportId, {
|
|
2192
2243
|
name: this.name,
|
|
2193
2244
|
pageSize: this.pageSize,
|
|
2194
2245
|
pageOrientation: this.pageOrientation,
|
|
2195
2246
|
layout: this.layout,
|
|
2196
2247
|
timezone: this.timezone,
|
|
2197
2248
|
styles: { heading: { size: this.headingSize } },
|
|
2198
|
-
})
|
|
2249
|
+
});
|
|
2250
|
+
const defaults$ = this.filterRows()
|
|
2251
|
+
.filter((row) => this._defaultChanged(row))
|
|
2252
|
+
.map((row) => this._reportData.updateFilterGroup(reportId, row.group.id, {
|
|
2253
|
+
default: this._rowDefault(row),
|
|
2254
|
+
}));
|
|
2255
|
+
return forkJoin([settings$, ...defaults$])
|
|
2199
2256
|
.pipe(tap(() => {
|
|
2200
2257
|
this._message.success('Report settings saved');
|
|
2201
2258
|
this._dialogRef.close({ action: 'saved' });
|
|
2202
2259
|
}));
|
|
2203
2260
|
};
|
|
2261
|
+
// Build one editable row per report-level filter group (level report or both),
|
|
2262
|
+
// in bar order, seeding each draft from the group's stored default.
|
|
2263
|
+
_buildFilterRows(report) {
|
|
2264
|
+
const reportId = report.id;
|
|
2265
|
+
const rows = (report.filterGroups ?? [])
|
|
2266
|
+
.filter((group) => group.level === 'report' || group.level === 'both')
|
|
2267
|
+
.sort((a, b) => a.order - b.order)
|
|
2268
|
+
.map((group) => {
|
|
2269
|
+
const groupDefault = group.config?.default;
|
|
2270
|
+
return {
|
|
2271
|
+
group,
|
|
2272
|
+
label: group.label || group.filters?.[0]?.filterColumn || 'Filter',
|
|
2273
|
+
start: dateFromBound(groupDefault?.start),
|
|
2274
|
+
end: dateFromBound(groupDefault?.end),
|
|
2275
|
+
values: (groupDefault?.values ?? []).map((value) => ({ name: String(value), value })),
|
|
2276
|
+
value: groupDefault?.value ?? '',
|
|
2277
|
+
// fs-autocomplete-chips fetch: the group's distinct options, name-filtered.
|
|
2278
|
+
// Same loader the report bar uses, so the two never drift.
|
|
2279
|
+
fetch: (keyword) => matchOptions(loadGroupOptions(group, this._reportData, reportId), keyword),
|
|
2280
|
+
};
|
|
2281
|
+
});
|
|
2282
|
+
this.filterRows.set(rows);
|
|
2283
|
+
}
|
|
2284
|
+
// The draft default normalized to the wire shape for this row's type, or null
|
|
2285
|
+
// when empty (which clears the stored default).
|
|
2286
|
+
_rowDefault(row) {
|
|
2287
|
+
switch (row.group.type) {
|
|
2288
|
+
case 'dateRange': {
|
|
2289
|
+
const start = dateToBound(row.start);
|
|
2290
|
+
const end = dateToBound(row.end);
|
|
2291
|
+
return (start || end) ? { start: start ?? undefined, end: end ?? undefined } : null;
|
|
2292
|
+
}
|
|
2293
|
+
case 'select':
|
|
2294
|
+
return row.values.length ? { values: row.values.map((option) => option.value) } : null;
|
|
2295
|
+
case 'keyword':
|
|
2296
|
+
default: {
|
|
2297
|
+
const value = row.value.trim();
|
|
2298
|
+
return value ? { value } : null;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
// Whether the row's draft differs from the group's stored default — only
|
|
2303
|
+
// changed rows are persisted on Save.
|
|
2304
|
+
_defaultChanged(row) {
|
|
2305
|
+
const before = JSON.stringify(this._normalize(row.group.config?.default ?? null));
|
|
2306
|
+
const after = JSON.stringify(this._normalize(this._rowDefault(row)));
|
|
2307
|
+
return before !== after;
|
|
2308
|
+
}
|
|
2309
|
+
// A stable comparison form: drop relative (not authored here) and undefined
|
|
2310
|
+
// keys so a no-op edit doesn't read as a change.
|
|
2311
|
+
_normalize(value) {
|
|
2312
|
+
if (!value) {
|
|
2313
|
+
return null;
|
|
2314
|
+
}
|
|
2315
|
+
const normalized = {};
|
|
2316
|
+
if (value.start) {
|
|
2317
|
+
normalized.start = value.start;
|
|
2318
|
+
}
|
|
2319
|
+
if (value.end) {
|
|
2320
|
+
normalized.end = value.end;
|
|
2321
|
+
}
|
|
2322
|
+
if (value.values?.length) {
|
|
2323
|
+
normalized.values = value.values;
|
|
2324
|
+
}
|
|
2325
|
+
if (value.value) {
|
|
2326
|
+
normalized.value = value.value;
|
|
2327
|
+
}
|
|
2328
|
+
if (value.relative) {
|
|
2329
|
+
normalized.relative = value.relative;
|
|
2330
|
+
}
|
|
2331
|
+
return Object.keys(normalized).length ? normalized : null;
|
|
2332
|
+
}
|
|
2204
2333
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2205
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.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 <mat-tab-group [(selected)]=\"selectedTab\">\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 <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\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-group>\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$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.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$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.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$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i4.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i4.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5.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$1, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2334
|
+
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 <mat-tab-group [(selected)]=\"selectedTab\">\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 <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\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 @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\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=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\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 }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\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-level 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 </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$3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$3.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$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.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$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.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$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i4$1.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i4.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i4.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i6.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i5.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5.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$1, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2206
2335
|
}
|
|
2207
2336
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, decorators: [{
|
|
2208
2337
|
type: Component,
|
|
@@ -2210,7 +2339,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2210
2339
|
FormsModule,
|
|
2211
2340
|
FsFormModule,
|
|
2212
2341
|
FsDialogModule,
|
|
2342
|
+
FsMessageModule,
|
|
2213
2343
|
FsTabsModule,
|
|
2344
|
+
FsDatePickerModule,
|
|
2345
|
+
FsAutocompleteChipsModule,
|
|
2214
2346
|
MatTabsModule,
|
|
2215
2347
|
MatButton,
|
|
2216
2348
|
MatDialogTitle,
|
|
@@ -2224,7 +2356,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2224
2356
|
MatSelect,
|
|
2225
2357
|
MatOption$1,
|
|
2226
2358
|
TimezoneSelectComponent,
|
|
2227
|
-
], 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 <mat-tab-group [(selected)]=\"selectedTab\">\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 <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\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-group>\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"] }]
|
|
2359
|
+
], 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 <mat-tab-group [(selected)]=\"selectedTab\">\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 <mat-form-field>\n <mat-label>\n Page Size\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageSize\"\n name=\"pageSize\">\n <mat-option value=\"widescreen\">\n Widescreen (13.33\" \u00D7 7.5\")\n </mat-option>\n <mat-option value=\"letter\">\n Letter (11\" \u00D7 8.5\")\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Orientation\n </mat-label>\n <mat-select\n [(ngModel)]=\"pageOrientation\"\n name=\"pageOrientation\">\n <mat-option value=\"landscape\">\n Landscape\n </mat-option>\n <mat-option value=\"portrait\">\n Portrait\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field>\n <mat-label>\n Layout\n </mat-label>\n <mat-select\n [(ngModel)]=\"layout\"\n name=\"layout\">\n <mat-option value=\"freeform\">\n Freeform \u2014 position components anywhere on the page\n </mat-option>\n <mat-option value=\"flow\">\n Flow \u2014 components flow into rows by width %\n </mat-option>\n </mat-select>\n <mat-hint>\n Freeform is like PowerPoint; Flow is like a responsive dashboard.\n </mat-hint>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\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 @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\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=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\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 }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\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-level 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 </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"] }]
|
|
2228
2360
|
}] });
|
|
2229
2361
|
|
|
2230
2362
|
// The assembled report structure returned by GET /api/reports/:id — composed
|
|
@@ -2336,7 +2468,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2336
2468
|
// complete synchronously after setOption.
|
|
2337
2469
|
async function renderChartImage(component, data, width, height, pixelRatio = 3) {
|
|
2338
2470
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
2339
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
2471
|
+
const echarts = (await import('./firestitch-report-echarts-m2yVum4J.mjs')).default;
|
|
2340
2472
|
const host = document.createElement('div');
|
|
2341
2473
|
host.style.width = `${width}px`;
|
|
2342
2474
|
host.style.height = `${height}px`;
|
|
@@ -2790,7 +2922,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2790
2922
|
|
|
2791
2923
|
// fs-filter query key for the runtime Frequency control (a render-layer bucket
|
|
2792
2924
|
// override, not a filter group — so it's read straight off the query).
|
|
2793
|
-
const FREQUENCY_ITEM = 'frequency';
|
|
2925
|
+
const FREQUENCY_ITEM$1 = 'frequency';
|
|
2794
2926
|
// The Reports page: AI chat on the left, the report canvas on the right —
|
|
2795
2927
|
// a split-pane layout; the viewer is our own canvas (no iframe, no embed URLs).
|
|
2796
2928
|
// The chat is always scoped to the selected report; its id rides on every
|
|
@@ -3028,7 +3160,7 @@ class ReportComponent {
|
|
|
3028
3160
|
: [];
|
|
3029
3161
|
if (reportId && this._hasTimeSeriesChart()) {
|
|
3030
3162
|
items.push({
|
|
3031
|
-
name: FREQUENCY_ITEM,
|
|
3163
|
+
name: FREQUENCY_ITEM$1,
|
|
3032
3164
|
type: ItemType.Select,
|
|
3033
3165
|
label: 'Frequency',
|
|
3034
3166
|
multiple: false,
|
|
@@ -3047,7 +3179,7 @@ class ReportComponent {
|
|
|
3047
3179
|
this._filterState.setValue(groupId, value);
|
|
3048
3180
|
}
|
|
3049
3181
|
// Frequency isn't a filter group — it's a render-layer bucket override.
|
|
3050
|
-
this._filterState.setFrequency(query?.[FREQUENCY_ITEM] || null);
|
|
3182
|
+
this._filterState.setFrequency(query?.[FREQUENCY_ITEM$1] || null);
|
|
3051
3183
|
},
|
|
3052
3184
|
};
|
|
3053
3185
|
}
|
|
@@ -3080,8 +3212,8 @@ class ReportComponent {
|
|
|
3080
3212
|
ReportPdfService,
|
|
3081
3213
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
3082
3214
|
// this route's chunk.
|
|
3083
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
3084
|
-
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div\n #split\n class=\"report fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n style=\"min-height: 500px;\"\n basePath=\"reports\"\n [requestData]=\"{ reportId: selected?.id ?? null }\"\n [introMessage]=\"introMessage\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n <div class=\"viewer fs-flex fs-column\">\n <div class=\"fs-row.align-center.gap-sm\">\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [fetch]=\"fetchReports\"\n [(ngModel)]=\"selected\"\n [disabled]=\"loadingReports\"\n [padless]=\"true\"\n [multiple]=\"false\"\n [fetchOnFocus]=\"true\"\n (ngModelChange)=\"reportChange($event)\"\n placeholder=\"Report\"\n name=\"report\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n <ng-template\n fsAutocompleteChipsStatic\n (click)=\"createReport()\">\n Create Report\n </ng-template>\n </fs-autocomplete-chips>\n @if (report) {\n <fs-menu>\n <ng-template\n fs-menu-item\n (click)=\"reportSettings()\">\n <mat-icon>\n tune\n </mat-icon>\n Report settings\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleEditMode()\">\n <mat-icon>\n {{ editMode ? 'lock' : 'open_with' }}\n </mat-icon>\n {{ editMode ? 'Done editing layout' : 'Edit layout' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n <mat-icon>\n slideshow\n </mat-icon>\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n <mat-icon>\n picture_as_pdf\n </mat-icon>\n Export PDF\n </ng-template>\n </fs-menu>\n }\n </div>\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the menu\n above). fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the block\n is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n</div>", styles: [".report{height:100%;min-height:500px}.report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:500px;min-width:0;border:none}.report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.report .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.report .resizer:before{content:\"\";width:0px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.report .resizer:hover:before{width:3px;background:var(--brand-primary-color)}.report.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.report.resizing .resizer:before{width:3px;background:var(--brand-primary-color)}.report.resizing .chat,.report.resizing .viewer{pointer-events:none}::ng-deep body.body-report-reports .mat-mdc-card-content{display:flex;flex-direction:column;box-sizing:border-box}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel{flex:1;min-height:0}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel router-outlet+ng-component{height:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$2.FsAutocompleteChipsStaticDirective, selector: "[fsAutocompleteChipsStatic]", inputs: ["show", "disable"], outputs: ["click", "selected"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type:
|
|
3215
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-m2yVum4J.mjs').then((module) => module.default) }),
|
|
3216
|
+
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }], ngImport: i0, template: "<div\n #split\n class=\"report fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n style=\"min-height: 500px;\"\n basePath=\"reports\"\n [requestData]=\"{ reportId: selected?.id ?? null }\"\n [introMessage]=\"introMessage\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n <div class=\"viewer fs-flex fs-column\">\n <div class=\"fs-row.align-center.gap-sm\">\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [fetch]=\"fetchReports\"\n [(ngModel)]=\"selected\"\n [disabled]=\"loadingReports\"\n [padless]=\"true\"\n [multiple]=\"false\"\n [fetchOnFocus]=\"true\"\n (ngModelChange)=\"reportChange($event)\"\n placeholder=\"Report\"\n name=\"report\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n <ng-template\n fsAutocompleteChipsStatic\n (click)=\"createReport()\">\n Create Report\n </ng-template>\n </fs-autocomplete-chips>\n @if (report) {\n <fs-menu>\n <ng-template\n fs-menu-item\n (click)=\"reportSettings()\">\n <mat-icon>\n tune\n </mat-icon>\n Report settings\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleEditMode()\">\n <mat-icon>\n {{ editMode ? 'lock' : 'open_with' }}\n </mat-icon>\n {{ editMode ? 'Done editing layout' : 'Edit layout' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n <mat-icon>\n slideshow\n </mat-icon>\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n <mat-icon>\n picture_as_pdf\n </mat-icon>\n Export PDF\n </ng-template>\n </fs-menu>\n }\n </div>\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the menu\n above). fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the block\n is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n</div>", styles: [".report{height:100%;min-height:500px}.report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:500px;min-width:0;border:none}.report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.report .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.report .resizer:before{content:\"\";width:0px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.report .resizer:hover:before{width:3px;background:var(--brand-primary-color)}.report.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.report.resizing .resizer:before{width:3px;background:var(--brand-primary-color)}.report.resizing .chat,.report.resizing .viewer{pointer-events:none}::ng-deep body.body-report-reports .mat-mdc-card-content{display:flex;flex-direction:column;box-sizing:border-box}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel{flex:1;min-height:0}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel router-outlet+ng-component{height:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$2.FsAutocompleteChipsStaticDirective, selector: "[fsAutocompleteChipsStatic]", inputs: ["show", "disable"], outputs: ["click", "selected"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3085
3217
|
}
|
|
3086
3218
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
|
|
3087
3219
|
type: Component,
|
|
@@ -3094,7 +3226,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3094
3226
|
ReportPdfService,
|
|
3095
3227
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
3096
3228
|
// this route's chunk.
|
|
3097
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
3229
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-m2yVum4J.mjs').then((module) => module.default) }),
|
|
3098
3230
|
], imports: [
|
|
3099
3231
|
FormsModule,
|
|
3100
3232
|
FsAutocompleteChipsModule,
|
|
@@ -3112,6 +3244,390 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3112
3244
|
args: ['chatPanel', { static: true, read: ElementRef }]
|
|
3113
3245
|
}] } });
|
|
3114
3246
|
|
|
3247
|
+
// The reports listing — an fs-list of reports. Rows are links built from a URL
|
|
3248
|
+
// pattern (a ':id' token → the report id; a leading '/' makes it absolute,
|
|
3249
|
+
// otherwise it's relative to this component's route). Create prompts for a
|
|
3250
|
+
// title, creates the report, then routes to it. The header gear emits so the
|
|
3251
|
+
// host can open its Semantic Database dialog. No routing is hard-coded here.
|
|
3252
|
+
class FsAiReportsComponent {
|
|
3253
|
+
// API base for the reports endpoints; override to mount elsewhere.
|
|
3254
|
+
basePath = input('reports');
|
|
3255
|
+
// Where a row links to; ':id' is replaced with the report id. Default takes
|
|
3256
|
+
// you to <current route>/:id. Absolute when it starts with '/'.
|
|
3257
|
+
openPattern = input(':id');
|
|
3258
|
+
// Where a freshly-created report routes to; same ':id' pattern rules.
|
|
3259
|
+
createPattern = input(':id');
|
|
3260
|
+
// The list-header gear — the host opens its Semantic Database dialog (the
|
|
3261
|
+
// builder lives in the app, so the package only signals the intent).
|
|
3262
|
+
semantic = output();
|
|
3263
|
+
list = null;
|
|
3264
|
+
listConfig;
|
|
3265
|
+
_reportData = inject(ReportData);
|
|
3266
|
+
_router = inject(Router);
|
|
3267
|
+
_route = inject(ActivatedRoute);
|
|
3268
|
+
_prompt = inject(FsPrompt);
|
|
3269
|
+
_destroyRef = inject(DestroyRef);
|
|
3270
|
+
constructor() {
|
|
3271
|
+
effect(() => {
|
|
3272
|
+
this._reportData.basePath = this.basePath();
|
|
3273
|
+
});
|
|
3274
|
+
this._initListConfig();
|
|
3275
|
+
}
|
|
3276
|
+
// routerLink commands for a report row (built from openPattern).
|
|
3277
|
+
openLink(report) {
|
|
3278
|
+
return this._patternCommands(this.openPattern(), report.id);
|
|
3279
|
+
}
|
|
3280
|
+
// Prompt for a title, create the report, then route to it (createPattern).
|
|
3281
|
+
createReport() {
|
|
3282
|
+
this._prompt.input({
|
|
3283
|
+
title: 'Create Report',
|
|
3284
|
+
label: 'Title',
|
|
3285
|
+
commitLabel: 'Create',
|
|
3286
|
+
required: true,
|
|
3287
|
+
})
|
|
3288
|
+
.pipe(switchMap((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
|
|
3289
|
+
.subscribe((report) => {
|
|
3290
|
+
const pattern = this.createPattern();
|
|
3291
|
+
this._router.navigate(this._patternCommands(pattern, report.id), pattern.startsWith('/') ? {} : { relativeTo: this._route });
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
// Turn a ':id' pattern into router commands. Relative unless it starts
|
|
3295
|
+
// with '/', in which case the first segment carries the leading slash.
|
|
3296
|
+
_patternCommands(pattern, id) {
|
|
3297
|
+
const absolute = pattern.startsWith('/');
|
|
3298
|
+
const segments = pattern.split('/')
|
|
3299
|
+
.filter((segment) => segment.length > 0)
|
|
3300
|
+
.map((segment) => (segment === ':id' ? id : segment));
|
|
3301
|
+
if (!segments.length) {
|
|
3302
|
+
return [id];
|
|
3303
|
+
}
|
|
3304
|
+
if (absolute) {
|
|
3305
|
+
segments[0] = `/${segments[0]}`;
|
|
3306
|
+
}
|
|
3307
|
+
return segments;
|
|
3308
|
+
}
|
|
3309
|
+
_initListConfig() {
|
|
3310
|
+
this.listConfig = {
|
|
3311
|
+
// The backend list is not paged yet; show the full set until it is.
|
|
3312
|
+
paging: false,
|
|
3313
|
+
actions: [
|
|
3314
|
+
{
|
|
3315
|
+
// Icon-only: opens the host's Semantic Database dialog.
|
|
3316
|
+
icon: 'settings',
|
|
3317
|
+
tooltip: 'Semantic database',
|
|
3318
|
+
primary: false,
|
|
3319
|
+
click: () => this.semantic.emit(),
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
label: 'Create',
|
|
3323
|
+
primary: true,
|
|
3324
|
+
click: () => this.createReport(),
|
|
3325
|
+
},
|
|
3326
|
+
],
|
|
3327
|
+
fetch: (query) => {
|
|
3328
|
+
return this._reportData.reports(query)
|
|
3329
|
+
.pipe(map((reports) => ({ data: reports ?? [] })));
|
|
3330
|
+
},
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3334
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.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 } }, outputs: { semantic: "semantic" }, viewQueries: [{ propertyName: "list", first: true, predicate: FsListComponent, descendants: true }], ngImport: i0, template: "<fs-list\n [config]=\"listConfig\"\n #list>\n\n <fs-list-column title=\"Name\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <a [routerLink]=\"openLink(row)\">{{ row.name }}</a>\n </ng-template>\n </fs-list-column>\n\n <fs-list-column title=\"Modified\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <fs-date [date]=\"row.modifyDate\"></fs-date>\n </ng-template>\n </fs-list-column>\n\n</fs-list>\n", styles: ["a{cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: FsDateModule }, { kind: "component", type: i1$4.FsDateComponent, selector: "fs-date", inputs: ["date", "format", "timezone"] }, { kind: "ngmodule", type: FsListModule }, { kind: "component", type: i1.FsListComponent, selector: "fs-list", inputs: ["config", "loaderLines", "cellRowType"], outputs: ["filtersReady"] }, { kind: "directive", type: i1.FsListColumnDirective, selector: "fs-list-column", inputs: ["show", "title", "name", "customizable", "sortable", "sortableDefault", "sortableDirection", "direction", "align", "width", "class"] }, { kind: "directive", type: i1.FsListCellDirective, selector: "[fs-list-cell],[fsListCell]", inputs: ["colspan", "align", "class", "fsListCell", "configTyping"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3335
|
+
}
|
|
3336
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
|
|
3337
|
+
type: Component,
|
|
3338
|
+
args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
|
|
3339
|
+
FsDateModule,
|
|
3340
|
+
FsListModule,
|
|
3341
|
+
RouterLink,
|
|
3342
|
+
], template: "<fs-list\n [config]=\"listConfig\"\n #list>\n\n <fs-list-column title=\"Name\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <a [routerLink]=\"openLink(row)\">{{ row.name }}</a>\n </ng-template>\n </fs-list-column>\n\n <fs-list-column title=\"Modified\">\n <ng-template\n fs-list-cell\n let-row=\"row\">\n <fs-date [date]=\"row.modifyDate\"></fs-date>\n </ng-template>\n </fs-list-column>\n\n</fs-list>\n", styles: ["a{cursor:pointer}\n"] }]
|
|
3343
|
+
}], ctorParameters: () => [], propDecorators: { list: [{
|
|
3344
|
+
type: ViewChild,
|
|
3345
|
+
args: [FsListComponent]
|
|
3346
|
+
}] } });
|
|
3347
|
+
|
|
3348
|
+
// fs-filter query key for the runtime Frequency control (a render-layer bucket
|
|
3349
|
+
// override, not a filter group — so it's read straight off the query).
|
|
3350
|
+
const FREQUENCY_ITEM = 'frequency';
|
|
3351
|
+
// A single report: the AI chat on the left, the report canvas on the right,
|
|
3352
|
+
// under an H2 title + settings gear. The host supplies [reportId] (from its
|
|
3353
|
+
// route) and switches reports by navigating — this component never picks one.
|
|
3354
|
+
class FsAiReportComponent {
|
|
3355
|
+
// The report this view is focused on — supplied by the host from the route.
|
|
3356
|
+
reportId = input(null);
|
|
3357
|
+
// API base for the reports + chat endpoints; override to mount elsewhere.
|
|
3358
|
+
basePath = input('reports');
|
|
3359
|
+
// The report was deleted from its settings dialog — the host routes back to
|
|
3360
|
+
// the listing.
|
|
3361
|
+
deleted = output();
|
|
3362
|
+
// The report was renamed (settings, or the agent) — the host can refresh a
|
|
3363
|
+
// breadcrumb / document title.
|
|
3364
|
+
renamed = output();
|
|
3365
|
+
report = null;
|
|
3366
|
+
editMode = false;
|
|
3367
|
+
// The report-level filter toolbar (fs-filter). Rebuilt whenever the loaded
|
|
3368
|
+
// report (and thus its groups) changes.
|
|
3369
|
+
reportFilterConfig;
|
|
3370
|
+
_split;
|
|
3371
|
+
_chatPanel;
|
|
3372
|
+
_reportData = inject(ReportData);
|
|
3373
|
+
_filterState = inject(ReportFilterStateService);
|
|
3374
|
+
_exportCollector = inject(ReportExportCollectorService);
|
|
3375
|
+
_pptxService = inject(ReportPptxService);
|
|
3376
|
+
_pdfService = inject(ReportPdfService);
|
|
3377
|
+
_process = inject(FsProcess);
|
|
3378
|
+
_dialog = inject(MatDialog);
|
|
3379
|
+
_destroyRef = inject(DestroyRef);
|
|
3380
|
+
_cdRef = inject(ChangeDetectorRef);
|
|
3381
|
+
_zone = inject(NgZone);
|
|
3382
|
+
constructor() {
|
|
3383
|
+
// Point the shared data gateway at the configured base first (before any
|
|
3384
|
+
// load below reads it).
|
|
3385
|
+
effect(() => {
|
|
3386
|
+
this._reportData.basePath = this.basePath();
|
|
3387
|
+
});
|
|
3388
|
+
// (Re)load whenever the host switches the report id.
|
|
3389
|
+
effect(() => {
|
|
3390
|
+
const reportId = this.reportId();
|
|
3391
|
+
this.editMode = false;
|
|
3392
|
+
if (reportId != null) {
|
|
3393
|
+
this._loadReport(reportId);
|
|
3394
|
+
}
|
|
3395
|
+
else {
|
|
3396
|
+
this.report = null;
|
|
3397
|
+
this._buildReportConfig();
|
|
3398
|
+
this._cdRef.markForCheck();
|
|
3399
|
+
}
|
|
3400
|
+
});
|
|
3401
|
+
this._buildReportConfig();
|
|
3402
|
+
}
|
|
3403
|
+
toggleEditMode() {
|
|
3404
|
+
this.editMode = !this.editMode;
|
|
3405
|
+
}
|
|
3406
|
+
// Report settings — name, page size, orientation, layout mode, delete. The
|
|
3407
|
+
// dialog performs the delete itself; we react to what it reports back.
|
|
3408
|
+
reportSettings() {
|
|
3409
|
+
if (!this.report) {
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
const reportId = this.report.id;
|
|
3413
|
+
this._dialog.open(ReportSettingsComponent, {
|
|
3414
|
+
data: { report: this.report },
|
|
3415
|
+
width: '460px',
|
|
3416
|
+
})
|
|
3417
|
+
.afterClosed()
|
|
3418
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
3419
|
+
.subscribe((result) => {
|
|
3420
|
+
if (result?.action === 'deleted') {
|
|
3421
|
+
this.deleted.emit(reportId);
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3424
|
+
if (result?.action === 'saved') {
|
|
3425
|
+
this._loadReport(reportId, true);
|
|
3426
|
+
}
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
// Component settings — title, exact geometry, and the diagnostic views
|
|
3430
|
+
// (SQL, config, filter wiring).
|
|
3431
|
+
componentSettings(component) {
|
|
3432
|
+
if (!this.report) {
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
const reportId = this.report.id;
|
|
3436
|
+
this._dialog.open(ComponentSettingsComponent, {
|
|
3437
|
+
data: { report: this.report, component },
|
|
3438
|
+
width: '640px',
|
|
3439
|
+
})
|
|
3440
|
+
.afterClosed()
|
|
3441
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
3442
|
+
.subscribe(() => {
|
|
3443
|
+
// Always reload: filter add/remove/level changes persist immediately
|
|
3444
|
+
// inside the dialog (no "save"), so the canvas + report bar must
|
|
3445
|
+
// refresh however the dialog was closed.
|
|
3446
|
+
this._loadReport(reportId);
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
// The canvas changed the report's structure (added a page) — re-fetch so the
|
|
3450
|
+
// new page renders; the canvas lands the view on it.
|
|
3451
|
+
onCanvasReportChanged() {
|
|
3452
|
+
if (this.report) {
|
|
3453
|
+
this._loadReport(this.report.id);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
// Export the report to PowerPoint, entirely client-side (native editable
|
|
3457
|
+
// charts; page = slide). The dock shows progress until the file saves.
|
|
3458
|
+
exportPowerpoint() {
|
|
3459
|
+
if (!this.report) {
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
const report = this.report;
|
|
3463
|
+
this._process.run('Exporting PowerPoint', this._exportCollector.collect(report)
|
|
3464
|
+
.pipe(switchMap((collected) => from(this._pptxService.export(collected)))));
|
|
3465
|
+
}
|
|
3466
|
+
// Export to PDF, entirely client-side (offscreen ECharts at 3×; page = PDF page).
|
|
3467
|
+
exportPdf() {
|
|
3468
|
+
if (!this.report) {
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
const report = this.report;
|
|
3472
|
+
this._process.run('Exporting PDF', this._exportCollector.collect(report)
|
|
3473
|
+
.pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
|
|
3474
|
+
}
|
|
3475
|
+
// React to each chat turn (fs-chat handles the request + persistence); we
|
|
3476
|
+
// only care about the report side effects this context's backend flags.
|
|
3477
|
+
onChatResponse(response) {
|
|
3478
|
+
const reportId = this.reportId();
|
|
3479
|
+
if (reportId == null) {
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
const reportResponse = response;
|
|
3483
|
+
// A structural change or a rename both re-fetch the report (session filter
|
|
3484
|
+
// values survive); a rename additionally notifies the host so it can
|
|
3485
|
+
// refresh a breadcrumb / title.
|
|
3486
|
+
if (reportResponse.reportChanged || reportResponse.reportRenamed) {
|
|
3487
|
+
this._loadReport(reportId, !!reportResponse.reportRenamed);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
// Drag the divider between the chat and canvas panes (pointer-capture loop
|
|
3491
|
+
// outside Angular).
|
|
3492
|
+
onResizeStart(event) {
|
|
3493
|
+
event.preventDefault();
|
|
3494
|
+
const handle = event.target;
|
|
3495
|
+
handle.setPointerCapture(event.pointerId);
|
|
3496
|
+
this._split.nativeElement.classList.add('resizing');
|
|
3497
|
+
this._zone.runOutsideAngular(() => {
|
|
3498
|
+
const move = (e) => this._resizeTo(e.clientX);
|
|
3499
|
+
const end = () => {
|
|
3500
|
+
handle.releasePointerCapture(event.pointerId);
|
|
3501
|
+
handle.removeEventListener('pointermove', move);
|
|
3502
|
+
handle.removeEventListener('pointerup', end);
|
|
3503
|
+
handle.removeEventListener('pointercancel', end);
|
|
3504
|
+
this._split.nativeElement.classList.remove('resizing');
|
|
3505
|
+
};
|
|
3506
|
+
handle.addEventListener('pointermove', move);
|
|
3507
|
+
handle.addEventListener('pointerup', end);
|
|
3508
|
+
handle.addEventListener('pointercancel', end);
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
_resizeTo(clientX) {
|
|
3512
|
+
const rect = this._split.nativeElement.getBoundingClientRect();
|
|
3513
|
+
if (rect.width === 0) {
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
const ratio = (clientX - rect.left) / rect.width;
|
|
3517
|
+
const clamped = Math.min(0.8, Math.max(0.2, ratio));
|
|
3518
|
+
this._chatPanel.nativeElement.style.flexBasis = `${clamped * 100}%`;
|
|
3519
|
+
}
|
|
3520
|
+
_loadReport(reportId, emitRenamed = false) {
|
|
3521
|
+
this._reportData.get(reportId)
|
|
3522
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
3523
|
+
.subscribe((report) => {
|
|
3524
|
+
this.report = report;
|
|
3525
|
+
this._filterState.init(report);
|
|
3526
|
+
this._buildReportConfig();
|
|
3527
|
+
if (emitRenamed) {
|
|
3528
|
+
this.renamed.emit(report);
|
|
3529
|
+
}
|
|
3530
|
+
this._cdRef.markForCheck();
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3533
|
+
// Report-level filter groups in their saved order — the ones that render in
|
|
3534
|
+
// the toolbar (component-level groups render on their components).
|
|
3535
|
+
_reportGroups() {
|
|
3536
|
+
return (this.report?.filterGroups ?? [])
|
|
3537
|
+
.filter((group) => group.level === 'report' || group.level === 'both')
|
|
3538
|
+
.sort((a, b) => a.order - b.order);
|
|
3539
|
+
}
|
|
3540
|
+
// Whether the loaded report has any report-level filters OR a time-series
|
|
3541
|
+
// chart (which earns the runtime Frequency control) — gates the toolbar.
|
|
3542
|
+
get reportHasFilters() {
|
|
3543
|
+
return this._reportGroups().length > 0 || this._hasTimeSeriesChart();
|
|
3544
|
+
}
|
|
3545
|
+
// fs-filter reads its config only once (at init). The template keys the
|
|
3546
|
+
// toolbar fs-filter on this signature so it's recreated (and re-reads the
|
|
3547
|
+
// rebuilt config) whenever the report-level filter set changes.
|
|
3548
|
+
get reportFilterKey() {
|
|
3549
|
+
return [
|
|
3550
|
+
...this._reportGroups().map((group) => `${group.id}:${group.level}:${group.label}`),
|
|
3551
|
+
this._hasTimeSeriesChart() ? 'frequency' : '',
|
|
3552
|
+
].join('|');
|
|
3553
|
+
}
|
|
3554
|
+
// (Re)build the toolbar fs-filter config — report-level filter items, plus
|
|
3555
|
+
// the runtime Frequency control when the report has a time-series chart.
|
|
3556
|
+
_buildReportConfig() {
|
|
3557
|
+
const reportId = this.report?.id ?? null;
|
|
3558
|
+
const items = reportId
|
|
3559
|
+
? this._reportGroups().map((group) => filterItemForGroup(group, this._reportData, reportId, this._filterState.value(group.id)))
|
|
3560
|
+
: [];
|
|
3561
|
+
if (reportId && this._hasTimeSeriesChart()) {
|
|
3562
|
+
items.push({
|
|
3563
|
+
name: FREQUENCY_ITEM,
|
|
3564
|
+
type: ItemType.Select,
|
|
3565
|
+
label: 'Frequency',
|
|
3566
|
+
multiple: false,
|
|
3567
|
+
values: () => FREQUENCY_OPTIONS,
|
|
3568
|
+
default: this._filterState.frequency() ?? undefined,
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
this.reportFilterConfig = {
|
|
3572
|
+
// Never touch the URL or persist filter state — report filters are
|
|
3573
|
+
// session-only, owned by ReportFilterStateService.
|
|
3574
|
+
queryParam: false,
|
|
3575
|
+
persist: false,
|
|
3576
|
+
items,
|
|
3577
|
+
change: (query) => {
|
|
3578
|
+
for (const { groupId, value } of groupValuesFromQuery(query ?? {}, this._reportGroups())) {
|
|
3579
|
+
this._filterState.setValue(groupId, value);
|
|
3580
|
+
}
|
|
3581
|
+
// Frequency isn't a filter group — it's a render-layer bucket override.
|
|
3582
|
+
this._filterState.setFrequency(query?.[FREQUENCY_ITEM] || null);
|
|
3583
|
+
},
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
// The report has at least one time-series chart (x-axis is a time axis) — the
|
|
3587
|
+
// only kind the Frequency control affects.
|
|
3588
|
+
_hasTimeSeriesChart() {
|
|
3589
|
+
return (this.report?.pages ?? []).some((page) => page.components.some((component) => component.type === 'chart' && component.config?.xAxis?.kind === 'time'));
|
|
3590
|
+
}
|
|
3591
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3592
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportComponent, isStandalone: true, selector: "fs-ai-report", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: true, isRequired: false, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { deleted: "deleted", renamed: "renamed" }, providers: [
|
|
3593
|
+
// Session filter state is per-open-report, not app-global; the export
|
|
3594
|
+
// services resolve filters through it, so they live at the same level.
|
|
3595
|
+
ReportFilterStateService,
|
|
3596
|
+
ReportExportCollectorService,
|
|
3597
|
+
ReportPptxService,
|
|
3598
|
+
ReportPdfService,
|
|
3599
|
+
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
3600
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-m2yVum4J.mjs').then((module) => module.default) }),
|
|
3601
|
+
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: 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 <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the header\n above). fs-filter reads its config once at init, so it's keyed on\n the 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 [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\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:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.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 .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.fs-ai-report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.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:1px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.fs-ai-report .resizer:hover:before{width:3px;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{width:3px;background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2$3.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2$3.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2$3.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "introMessage"], outputs: ["response"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode"], outputs: ["componentSettings", "reportChanged", "editDone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3602
|
+
}
|
|
3603
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
|
|
3604
|
+
type: Component,
|
|
3605
|
+
args: [{ selector: 'fs-ai-report', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [
|
|
3606
|
+
// Session filter state is per-open-report, not app-global; the export
|
|
3607
|
+
// services resolve filters through it, so they live at the same level.
|
|
3608
|
+
ReportFilterStateService,
|
|
3609
|
+
ReportExportCollectorService,
|
|
3610
|
+
ReportPptxService,
|
|
3611
|
+
ReportPdfService,
|
|
3612
|
+
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
3613
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-m2yVum4J.mjs').then((module) => module.default) }),
|
|
3614
|
+
], imports: [
|
|
3615
|
+
FsFilterModule,
|
|
3616
|
+
FsMenuModule,
|
|
3617
|
+
FsAiChatComponent,
|
|
3618
|
+
MatButtonModule,
|
|
3619
|
+
MatIconModule,
|
|
3620
|
+
MatTooltipModule,
|
|
3621
|
+
ReportCanvasComponent,
|
|
3622
|
+
], 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 <button\n mat-icon-button\n type=\"button\"\n [matTooltip]=\"editMode ? 'Done' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>{{ editMode ? 'lock' : 'open_with' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"{ reportId: reportId() }\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report) {\n <!-- Report-level filters only (the report's actions live in the header\n above). fs-filter reads its config once at init, so it's keyed on\n the 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 [config]=\"reportFilterConfig\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report\"\n [editMode]=\"editMode\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (editDone)=\"toggleEditMode()\">\n </app-report-canvas>\n }\n </div>\n </div>\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:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.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 .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.fs-ai-report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.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:1px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.fs-ai-report .resizer:hover:before{width:3px;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{width:3px;background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
|
|
3623
|
+
}], ctorParameters: () => [], propDecorators: { _split: [{
|
|
3624
|
+
type: ViewChild,
|
|
3625
|
+
args: ['split', { static: true }]
|
|
3626
|
+
}], _chatPanel: [{
|
|
3627
|
+
type: ViewChild,
|
|
3628
|
+
args: ['chatPanel', { static: true, read: ElementRef }]
|
|
3629
|
+
}] } });
|
|
3630
|
+
|
|
3115
3631
|
/*
|
|
3116
3632
|
* Public API Surface of @firestitch/reports
|
|
3117
3633
|
*
|
|
@@ -3124,5 +3640,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3124
3640
|
* Generated bundle index. Do not edit.
|
|
3125
3641
|
*/
|
|
3126
3642
|
|
|
3127
|
-
export {
|
|
3128
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
3643
|
+
export { FsAiReportsComponent as F, REPORT_CHART_COLORS_CSS as R, ReportComponent as a, FsAiReportComponent as b, ReportData as c, ReportService as d, ReportFilterStateService as e, FREQUENCY_OPTIONS as f };
|
|
3644
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-BclPOljK.mjs.map
|