@firestitch/report 18.0.19 → 18.0.21
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 +8 -2
- package/app/reports/dialogs/report-settings/report-settings.component.d.ts +4 -2
- package/app/reports/interfaces/report.interface.d.ts +26 -2
- package/app/reports/layout.d.ts +2 -0
- package/app/reports/services/report-filter-state.service.d.ts +4 -1
- package/app/reports/views/report/fs-ai-report.component.d.ts +2 -0
- package/app/reports/views/report/report.component.d.ts +2 -0
- package/app/reports/views/reports/fs-ai-reports.component.d.ts +28 -8
- package/esm2022/app/reports/data/report.data.mjs +53 -4
- package/esm2022/app/reports/dialogs/report-settings/report-settings.component.mjs +16 -11
- package/esm2022/app/reports/interfaces/report.interface.mjs +12 -1
- package/esm2022/app/reports/layout.mjs +6 -1
- package/esm2022/app/reports/services/report-filter-state.service.mjs +55 -4
- package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +27 -5
- package/esm2022/app/reports/views/report/report.component.mjs +27 -5
- package/esm2022/app/reports/views/reports/fs-ai-reports.component.mjs +342 -74
- package/fesm2022/{firestitch-report-echarts-CfwgLaOA.mjs → firestitch-report-echarts-BYmAhi-N.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-CfwgLaOA.mjs.map → firestitch-report-echarts-BYmAhi-N.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-_MCMM4XH.mjs → firestitch-report-firestitch-report-acZuB8V2.mjs} +529 -99
- package/fesm2022/firestitch-report-firestitch-report-acZuB8V2.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/firestitch-report-firestitch-report-_MCMM4XH.mjs.map +0 -1
|
@@ -13,12 +13,12 @@ import * as i2$3 from '@firestitch/menu';
|
|
|
13
13
|
import { FsMenuModule } from '@firestitch/menu';
|
|
14
14
|
import { FsProcess } from '@firestitch/process';
|
|
15
15
|
import { FsPrompt } from '@firestitch/prompt';
|
|
16
|
-
import { of, Subject, map as map$1, debounceTime, forkJoin, BehaviorSubject, from } from 'rxjs';
|
|
16
|
+
import { of, Subject, map as map$1, debounceTime, forkJoin, BehaviorSubject, from, switchMap as switchMap$1 } from 'rxjs';
|
|
17
17
|
import { map, shareReplay, filter, tap, switchMap, catchError, finalize, take } from 'rxjs/operators';
|
|
18
18
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
19
19
|
import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
|
|
20
20
|
import { FsAiChatComponent } from '@firestitch/ai';
|
|
21
|
-
import * as i3$
|
|
21
|
+
import * as i3$2 from '@angular/material/button';
|
|
22
22
|
import { MatIconButton, MatButton, MatButtonModule } from '@angular/material/button';
|
|
23
23
|
import * as i2$5 from '@angular/material/tooltip';
|
|
24
24
|
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
|
|
@@ -51,12 +51,14 @@ import { FsDatePickerModule } from '@firestitch/datepicker';
|
|
|
51
51
|
import { MatOption } from '@angular/material/core';
|
|
52
52
|
import { guid } from '@firestitch/common';
|
|
53
53
|
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
|
54
|
-
import * as i2$4 from '@firestitch/date';
|
|
55
|
-
import { FsDateModule } from '@firestitch/date';
|
|
56
54
|
import * as i4$1 from '@firestitch/skeleton';
|
|
57
55
|
import { FsSkeletonModule } from '@firestitch/skeleton';
|
|
56
|
+
import * as i3$1 from '@firestitch/tree';
|
|
57
|
+
import { FsTreeComponent, TreeDragMode, TreeActionType, FsTreeModule } from '@firestitch/tree';
|
|
58
58
|
import * as i1$4 from '@firestitch/chip';
|
|
59
59
|
import { FsChipModule } from '@firestitch/chip';
|
|
60
|
+
import * as i2$4 from '@firestitch/date';
|
|
61
|
+
import { FsDateModule } from '@firestitch/date';
|
|
60
62
|
import { FsMarkdownRendererComponent, FsMarkdownEditorComponent } from '@firestitch/markdown-editor';
|
|
61
63
|
import * as i1$5 from '@angular/material/expansion';
|
|
62
64
|
import { MatExpansionModule } from '@angular/material/expansion';
|
|
@@ -80,20 +82,58 @@ class ReportData {
|
|
|
80
82
|
...config,
|
|
81
83
|
});
|
|
82
84
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
// Both halves of the listing — reports AND the folders they are filed into —
|
|
86
|
+
// from the one GET. No `key`, because the whole payload is the answer here;
|
|
87
|
+
// reports() keeps its 'reports' key for callers that only want the flat list.
|
|
88
|
+
tree(query = {}, config = {}) {
|
|
89
|
+
return this._api.get(this._path(), query, config);
|
|
90
|
+
}
|
|
91
|
+
create(name, reportFolderId = null, config = {}) {
|
|
92
|
+
return this._api.post(this._path(), { name, reportFolderId }, {
|
|
85
93
|
key: 'report',
|
|
86
94
|
...config,
|
|
87
95
|
});
|
|
88
96
|
}
|
|
97
|
+
// File a report under a folder, or pass null to move it to the root. Sent
|
|
98
|
+
// explicitly (never omitted) so the backend can tell "move to root" from
|
|
99
|
+
// "leave the folder alone".
|
|
100
|
+
moveReport(reportId, reportFolderId, config = {}) {
|
|
101
|
+
return this._api.put(this._path(`${reportId}`), { reportFolderId }, {
|
|
102
|
+
key: 'report',
|
|
103
|
+
...config,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
createFolder(name, parentReportFolderId = null, config = {}) {
|
|
107
|
+
return this._api.post(this._path('folders'), { name, parentReportFolderId }, {
|
|
108
|
+
key: 'folder',
|
|
109
|
+
...config,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Rename and/or move a folder. Only the keys passed are acted on server-side,
|
|
113
|
+
// so a rename never disturbs the parent and a drag never disturbs the name.
|
|
114
|
+
updateFolder(reportFolderId, folder, config = {}) {
|
|
115
|
+
return this._api.put(this._path(`folders/${reportFolderId}`), folder, {
|
|
116
|
+
key: 'folder',
|
|
117
|
+
...config,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// A real delete — the backend refuses while the folder still holds a
|
|
121
|
+
// sub-folder or an active report, so there is no undelete counterpart.
|
|
122
|
+
deleteFolder(reportFolderId, config = {}) {
|
|
123
|
+
return this._api.delete(this._path(`folders/${reportFolderId}`), {}, {
|
|
124
|
+
key: 'folder',
|
|
125
|
+
...config,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
89
128
|
rename(reportId, name, config = {}) {
|
|
90
129
|
return this._api.put(this._path(`${reportId}`), { name }, {
|
|
91
130
|
key: 'report',
|
|
92
131
|
...config,
|
|
93
132
|
});
|
|
94
133
|
}
|
|
95
|
-
// Report settings: any subset of { name,
|
|
134
|
+
// Report settings: any subset of { name, pageWidth, pageHeight, layout,
|
|
96
135
|
// styles } — styles carry the report's typographic config (e.g. heading size).
|
|
136
|
+
// The page size is in inches and width/height go together.
|
|
97
137
|
update(reportId, settings, config = {}) {
|
|
98
138
|
return this._api.put(this._path(`${reportId}`), settings, {
|
|
99
139
|
key: 'report',
|
|
@@ -165,6 +205,17 @@ class ReportData {
|
|
|
165
205
|
...config,
|
|
166
206
|
});
|
|
167
207
|
}
|
|
208
|
+
// Exports EVERY component on the report — or on one page — server-side and
|
|
209
|
+
// resolves to a presigned URL for a zip of one CSV per component, foldered by
|
|
210
|
+
// page. `filters` carries the resolved values for every component together;
|
|
211
|
+
// each one picks out its own. Same keep-alive contract as exportComponent,
|
|
212
|
+
// over a longer run; consume through FsProcess.download.
|
|
213
|
+
exportReport(reportId, filters = [], pageId = null, config = {}) {
|
|
214
|
+
return this._api.post(this._path(`${reportId}/export`), { filters, ...(pageId ? { pageId } : {}) }, {
|
|
215
|
+
key: 'url',
|
|
216
|
+
...config,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
168
219
|
// Distinct options for a select filter (drives multiselect controls).
|
|
169
220
|
filterOptions(reportId, filterId, config = {}) {
|
|
170
221
|
return this._api.get(this._path(`${reportId}/filters/${filterId}/options`), {}, {
|
|
@@ -338,6 +389,11 @@ function pageGeometry(report, page) {
|
|
|
338
389
|
}
|
|
339
390
|
return geometry;
|
|
340
391
|
}
|
|
392
|
+
// Bounds for a page dimension, in inches. Mirrors ReportModel::PAGE_DIMENSION_MIN
|
|
393
|
+
// / MAX on the server, which is the enforcing side — these drive the field-level
|
|
394
|
+
// message so the operator sees the limit before submitting.
|
|
395
|
+
const PAGE_DIMENSION_MIN = 1;
|
|
396
|
+
const PAGE_DIMENSION_MAX = 200;
|
|
341
397
|
|
|
342
398
|
// A date-range boundary as the picked LOCAL calendar day (no time, no tz).
|
|
343
399
|
// Date-range filters are calendar-day semantics, so the boundary must carry
|
|
@@ -478,6 +534,36 @@ function selectQueryValues(raw) {
|
|
|
478
534
|
return String(raw).split(',');
|
|
479
535
|
}
|
|
480
536
|
|
|
537
|
+
// Whether two stored filter-group defaults are the same. Compared on the fields a
|
|
538
|
+
// default can actually carry, so key order in the payload can't read as a change.
|
|
539
|
+
function sameDefault(a, b) {
|
|
540
|
+
if (!a || !b) {
|
|
541
|
+
return !a && !b;
|
|
542
|
+
}
|
|
543
|
+
return a.start === b.start
|
|
544
|
+
&& a.end === b.end
|
|
545
|
+
&& a.value === b.value
|
|
546
|
+
&& a.relative === b.relative
|
|
547
|
+
&& (a.values ?? []).length === (b.values ?? []).length
|
|
548
|
+
&& (a.values ?? []).every((value, index) => value === (b.values ?? [])[index]);
|
|
549
|
+
}
|
|
550
|
+
// A stable signature for a group's stored default. The report toolbar keys its
|
|
551
|
+
// fs-filter on this, because fs-filter reads its config once at init — without a
|
|
552
|
+
// key change it keeps rendering the values it was created with even after the
|
|
553
|
+
// config behind it has been rebuilt.
|
|
554
|
+
function defaultSignature(group) {
|
|
555
|
+
const groupDefault = group.config?.default;
|
|
556
|
+
if (!groupDefault) {
|
|
557
|
+
return '';
|
|
558
|
+
}
|
|
559
|
+
return [
|
|
560
|
+
groupDefault.start ?? '',
|
|
561
|
+
groupDefault.end ?? '',
|
|
562
|
+
groupDefault.value ?? '',
|
|
563
|
+
groupDefault.relative ?? '',
|
|
564
|
+
(groupDefault.values ?? []).join(','),
|
|
565
|
+
].join('~');
|
|
566
|
+
}
|
|
481
567
|
// Session filter state for the open report. Filter VALUES are never persisted
|
|
482
568
|
// — this service holds them per filter GROUP (the propagation unit), resolves
|
|
483
569
|
// them into per-component member-filter values for each data request, and
|
|
@@ -501,18 +587,28 @@ class ReportFilterStateService {
|
|
|
501
587
|
_changed$ = new Subject();
|
|
502
588
|
// Seed groups + their defaults when a report loads. Existing session values
|
|
503
589
|
// for still-existing groups survive a structure re-fetch (a chat edit
|
|
504
|
-
// shouldn't reset the viewer's filters)
|
|
590
|
+
// shouldn't reset the viewer's filters) — UNLESS the group's stored default
|
|
591
|
+
// itself changed, which only happens when an operator saved a new default in
|
|
592
|
+
// report settings. That is an explicit instruction to re-seed, so it wins over
|
|
593
|
+
// the surviving session value; without it the bar keeps showing whatever it was
|
|
594
|
+
// first seeded with until a full page reload.
|
|
505
595
|
init(report) {
|
|
506
|
-
this._groups
|
|
596
|
+
const previousGroups = this._groups;
|
|
597
|
+
this._groups = new Map();
|
|
507
598
|
const validIds = new Set();
|
|
508
599
|
for (const group of report.filterGroups ?? []) {
|
|
509
600
|
this._groups.set(group.id, group);
|
|
510
601
|
validIds.add(group.id);
|
|
511
|
-
|
|
602
|
+
const defaultChanged = !sameDefault(previousGroups.get(group.id)?.config?.default ?? null, group.config?.default ?? null);
|
|
603
|
+
if (!this._values.has(group.id) || defaultChanged) {
|
|
512
604
|
const defaultValue = this._defaultValue(group);
|
|
513
605
|
if (defaultValue) {
|
|
514
606
|
this._values.set(group.id, defaultValue);
|
|
515
607
|
}
|
|
608
|
+
else if (defaultChanged) {
|
|
609
|
+
// The default was cleared, so the value it seeded goes with it.
|
|
610
|
+
this._values.delete(group.id);
|
|
611
|
+
}
|
|
516
612
|
}
|
|
517
613
|
}
|
|
518
614
|
for (const groupId of [...this._values.keys()]) {
|
|
@@ -599,6 +695,17 @@ class ReportFilterStateService {
|
|
|
599
695
|
}
|
|
600
696
|
return resolved;
|
|
601
697
|
}
|
|
698
|
+
// Every component's resolved values in one array — what a report-level export
|
|
699
|
+
// carries so each file in the archive is filtered exactly as the screen is.
|
|
700
|
+
// Filter ids are unique per component, so the backend picks each component's
|
|
701
|
+
// own out of the pile. A page id narrows it to that page.
|
|
702
|
+
resolveForReport(report, pageId = null) {
|
|
703
|
+
const pages = (report.pages ?? [])
|
|
704
|
+
.filter((page) => !pageId || page.id === pageId);
|
|
705
|
+
return pages
|
|
706
|
+
.flatMap((page) => (page.components ?? [])
|
|
707
|
+
.flatMap((component) => this.resolveForComponent(component)));
|
|
708
|
+
}
|
|
602
709
|
// A group's authored default, mapped to a session value by type. Relative
|
|
603
710
|
// date presets resolve against "now" each time the report opens, so a saved
|
|
604
711
|
// "last 12 months" stays relative; a fixed range, select values and keyword
|
|
@@ -2317,7 +2424,7 @@ function dateToBound(value) {
|
|
|
2317
2424
|
}
|
|
2318
2425
|
|
|
2319
2426
|
// Report settings, in three tabs:
|
|
2320
|
-
// Settings — name + page setup (size
|
|
2427
|
+
// Settings — name + page setup (size in inches, layout mode).
|
|
2321
2428
|
// Styles — the report's typographic styles (sizes in POINTS, 1:1 with
|
|
2322
2429
|
// PDF/PowerPoint); the Heading size to start.
|
|
2323
2430
|
// Filters — the report-level filters and their DEFAULT values. A default
|
|
@@ -2328,8 +2435,13 @@ function dateToBound(value) {
|
|
|
2328
2435
|
class ReportSettingsComponent {
|
|
2329
2436
|
selectedTab = 'settings';
|
|
2330
2437
|
name = '';
|
|
2331
|
-
|
|
2332
|
-
|
|
2438
|
+
// The page in inches. No initializer: every report carries a size, and
|
|
2439
|
+
// ngOnInit seeds both before the tab renders, so a placeholder here could only
|
|
2440
|
+
// ever be a wrong number briefly showing as the operator's own.
|
|
2441
|
+
pageWidth;
|
|
2442
|
+
pageHeight;
|
|
2443
|
+
pageDimensionMin = PAGE_DIMENSION_MIN;
|
|
2444
|
+
pageDimensionMax = PAGE_DIMENSION_MAX;
|
|
2333
2445
|
layout = 'freeform';
|
|
2334
2446
|
// Empty until the report has one; app-timezone-select then defaults it to the
|
|
2335
2447
|
// viewer's browser zone (which Save persists, making the report's zone explicit).
|
|
@@ -2346,8 +2458,8 @@ class ReportSettingsComponent {
|
|
|
2346
2458
|
ngOnInit() {
|
|
2347
2459
|
const report = this._data.report;
|
|
2348
2460
|
this.name = report.name;
|
|
2349
|
-
this.
|
|
2350
|
-
this.
|
|
2461
|
+
this.pageWidth = report.pageWidth;
|
|
2462
|
+
this.pageHeight = report.pageHeight;
|
|
2351
2463
|
this.layout = report.layout ?? 'freeform';
|
|
2352
2464
|
this.timezone = report.timezone ?? '';
|
|
2353
2465
|
this.headingSize = report.config?.styles?.heading?.size ?? DEFAULT_HEADING_SIZE;
|
|
@@ -2374,8 +2486,8 @@ class ReportSettingsComponent {
|
|
|
2374
2486
|
// result, re-seeding the bar from the new defaults.
|
|
2375
2487
|
const settings$ = this._reportData.update(reportId, {
|
|
2376
2488
|
name: this.name,
|
|
2377
|
-
|
|
2378
|
-
|
|
2489
|
+
pageWidth: this.pageWidth,
|
|
2490
|
+
pageHeight: this.pageHeight,
|
|
2379
2491
|
layout: this.layout,
|
|
2380
2492
|
timezone: this.timezone,
|
|
2381
2493
|
styles: { heading: { size: this.headingSize } },
|
|
@@ -2485,7 +2597,7 @@ class ReportSettingsComponent {
|
|
|
2485
2597
|
return Object.keys(normalized).length ? normalized : null;
|
|
2486
2598
|
}
|
|
2487
2599
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2488
|
-
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.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: 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$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5$1.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 });
|
|
2600
|
+
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 <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <span matTextSuffix>\n inches\n </span>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <span matTextSuffix>\n inches\n </span>\n </mat-form-field>\n </div>\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.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i5.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i5.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: 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$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i5$1.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 });
|
|
2489
2601
|
}
|
|
2490
2602
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, decorators: [{
|
|
2491
2603
|
type: Component,
|
|
@@ -2510,7 +2622,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2510
2622
|
MatSelect,
|
|
2511
2623
|
MatOption$1,
|
|
2512
2624
|
TimezoneSelectComponent,
|
|
2513
|
-
], 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
|
|
2625
|
+
], 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 <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <span matTextSuffix>\n inches\n </span>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"0.1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <span matTextSuffix>\n inches\n </span>\n </mat-form-field>\n </div>\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"] }]
|
|
2514
2626
|
}] });
|
|
2515
2627
|
|
|
2516
2628
|
// The assembled report structure returned by GET /api/reports/:id — composed
|
|
@@ -2528,13 +2640,24 @@ const FREQUENCY_OPTIONS = [
|
|
|
2528
2640
|
{ value: 'quarter', name: 'Quarter' },
|
|
2529
2641
|
{ value: 'year', name: 'Annually' },
|
|
2530
2642
|
];
|
|
2643
|
+
var ReportNodeType;
|
|
2644
|
+
(function (ReportNodeType) {
|
|
2645
|
+
ReportNodeType["Folder"] = "folder";
|
|
2646
|
+
ReportNodeType["Report"] = "report";
|
|
2647
|
+
})(ReportNodeType || (ReportNodeType = {}));
|
|
2531
2648
|
// Reports are soft-deletable: a deleted report leaves the listing but keeps its
|
|
2532
2649
|
// pages and components, and the listing's "Show Deleted" mode restores it.
|
|
2650
|
+
// Folders have no equivalent — they are only deletable once empty, so they hard
|
|
2651
|
+
// delete and there is nothing to restore.
|
|
2533
2652
|
var ReportState;
|
|
2534
2653
|
(function (ReportState) {
|
|
2535
2654
|
ReportState["Active"] = "active";
|
|
2536
2655
|
ReportState["Deleted"] = "deleted";
|
|
2537
2656
|
})(ReportState || (ReportState = {}));
|
|
2657
|
+
const ReportStates = [
|
|
2658
|
+
{ name: 'Active', value: ReportState.Active },
|
|
2659
|
+
{ name: 'Deleted', value: ReportState.Deleted },
|
|
2660
|
+
];
|
|
2538
2661
|
|
|
2539
2662
|
// Rows fetched for a list component's export table — enough to fill a
|
|
2540
2663
|
// slide/page table; the cap note covers the rest.
|
|
@@ -2648,7 +2771,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
2648
2771
|
// complete synchronously after setOption.
|
|
2649
2772
|
async function renderChartImage(component, data, width, height, pixelRatio = 3) {
|
|
2650
2773
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
2651
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
2774
|
+
const echarts = (await import('./firestitch-report-echarts-BYmAhi-N.mjs')).default;
|
|
2652
2775
|
const host = document.createElement('div');
|
|
2653
2776
|
host.style.width = `${width}px`;
|
|
2654
2777
|
host.style.height = `${height}px`;
|
|
@@ -3122,6 +3245,9 @@ class ReportComponent {
|
|
|
3122
3245
|
reportFilterConfig;
|
|
3123
3246
|
_split;
|
|
3124
3247
|
_chatPanel;
|
|
3248
|
+
// Only the canvas knows which page is on screen, and a page-scoped export
|
|
3249
|
+
// has to ask it.
|
|
3250
|
+
_canvas;
|
|
3125
3251
|
introMessage = {
|
|
3126
3252
|
text: '',
|
|
3127
3253
|
};
|
|
@@ -3254,6 +3380,19 @@ class ReportComponent {
|
|
|
3254
3380
|
this._process.run('Exporting PDF', this._exportCollector.collect(report)
|
|
3255
3381
|
.pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
|
|
3256
3382
|
}
|
|
3383
|
+
// The raw data behind the whole report in one action: the server exports every
|
|
3384
|
+
// component's full filtered dataset and zips one CSV per component, foldered
|
|
3385
|
+
// by page. `thisPage` narrows it to the page the canvas is showing. Unlike the
|
|
3386
|
+
// PDF/PowerPoint exports this is entirely server-side — nothing is collected
|
|
3387
|
+
// in the browser, so it is not capped by what a slide can hold.
|
|
3388
|
+
exportData(thisPage = false) {
|
|
3389
|
+
const report = this.report;
|
|
3390
|
+
if (!report) {
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
const pageId = thisPage ? this._canvas?.activePage?.id ?? null : null;
|
|
3394
|
+
this._process.download('Exporting data', this._reportData.exportReport(report.id, this._filterState.resolveForReport(report, pageId), pageId));
|
|
3395
|
+
}
|
|
3257
3396
|
// React to each chat turn (fs-chat handles the request + persistence); we
|
|
3258
3397
|
// only care about the report side effects this context's backend flags.
|
|
3259
3398
|
onChatResponse(response) {
|
|
@@ -3327,7 +3466,10 @@ class ReportComponent {
|
|
|
3327
3466
|
// changes, e.g. after the component settings dialog adds/moves a filter.
|
|
3328
3467
|
get reportFilterKey() {
|
|
3329
3468
|
return [
|
|
3330
|
-
|
|
3469
|
+
// The stored default is part of the signature: saving a new default in report
|
|
3470
|
+
// settings rebuilds the config, but without a key change fs-filter keeps
|
|
3471
|
+
// rendering the values it was created with until a full page reload.
|
|
3472
|
+
...this._reportGroups().map((group) => `${group.id}:${group.level}:${group.label}:${defaultSignature(group)}`),
|
|
3331
3473
|
this._hasTimeSeriesChart() ? 'frequency' : '',
|
|
3332
3474
|
].join('|');
|
|
3333
3475
|
}
|
|
@@ -3394,8 +3536,8 @@ class ReportComponent {
|
|
|
3394
3536
|
ReportPdfService,
|
|
3395
3537
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
3396
3538
|
// this route's chunk.
|
|
3397
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
3398
|
-
], 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 [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\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", "showThinking"], outputs: ["response", "showThinkingChange"] }, { 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 });
|
|
3539
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-BYmAhi-N.mjs').then((module) => module.default) }),
|
|
3540
|
+
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: 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 [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\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 <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data (This Page)\n </ng-template>\n }\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", "showThinking"], outputs: ["response", "showThinkingChange"] }, { 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 });
|
|
3399
3541
|
}
|
|
3400
3542
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
|
|
3401
3543
|
type: Component,
|
|
@@ -3408,7 +3550,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3408
3550
|
ReportPdfService,
|
|
3409
3551
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
3410
3552
|
// this route's chunk.
|
|
3411
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
3553
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-BYmAhi-N.mjs').then((module) => module.default) }),
|
|
3412
3554
|
], imports: [
|
|
3413
3555
|
FormsModule,
|
|
3414
3556
|
FsAutocompleteChipsModule,
|
|
@@ -3417,13 +3559,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
3417
3559
|
FsAiChatComponent,
|
|
3418
3560
|
MatIcon,
|
|
3419
3561
|
ReportCanvasComponent,
|
|
3420
|
-
], 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 [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\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"] }]
|
|
3562
|
+
], 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 [disabled]=\"editMode\">\n <mat-icon>\n dashboard_2_edit\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 <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data (This Page)\n </ng-template>\n }\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"] }]
|
|
3421
3563
|
}], ctorParameters: () => [], propDecorators: { _split: [{
|
|
3422
3564
|
type: ViewChild,
|
|
3423
3565
|
args: ['split', { static: true }]
|
|
3424
3566
|
}], _chatPanel: [{
|
|
3425
3567
|
type: ViewChild,
|
|
3426
3568
|
args: ['chatPanel', { static: true, read: ElementRef }]
|
|
3569
|
+
}], _canvas: [{
|
|
3570
|
+
type: ViewChild,
|
|
3571
|
+
args: [ReportCanvasComponent]
|
|
3427
3572
|
}] } });
|
|
3428
3573
|
|
|
3429
3574
|
// The AI Knowledge document's version history (append-only), each version
|
|
@@ -4050,18 +4195,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4050
4195
|
], template: "<form\n *fsSkeletonForm=\"loaded()\"\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n Report Settings\n </fs-dialog-title>\n <mat-dialog-content>\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"AI Knowledge\"\n name=\"knowledge\">\n <fs-ai-report-global-settings-ai-knowledge\n [content]=\"content()\"\n (contentChange)=\"content.set($event)\"\n [config]=\"editorConfig\">\n </fs-ai-report-global-settings-ai-knowledge>\n </mat-tab>\n <mat-tab\n label=\"Semantic Database\"\n name=\"build\">\n <fs-ai-report-global-settings-semantic-database>\n </fs-ai-report-global-settings-semantic-database>\n </mat-tab>\n <mat-tab\n label=\"General\"\n name=\"general\">\n <fs-ai-report-global-settings-general\n [concurrency]=\"concurrency()\"\n (concurrencyChange)=\"concurrency.set($event)\"\n [min]=\"settings()!.semanticBuildConcurrencyMin\"\n [max]=\"settings()!.semanticBuildConcurrencyMax\">\n </fs-ai-report-global-settings-general>\n </mat-tab>\n </mat-tab-group>\n </mat-dialog-content>\n <mat-dialog-actions>\n <!-- The footer follows the selected tab. AI Knowledge projects its own\n Save | View History | Cancel row (fs-form-dialog-actions renders\n projected content after Save/Cancel, so the default pair can't keep\n View History beside Save); General uses the default Save/Cancel; the\n Semantic Database tab is a read/run surface, so it gets a primary\n Close plus its Build/Rebuild/Stop action. -->\n <fs-form-dialog-actions [save]=\"selectedTab === 'general'\">\n @if (selectedTab === 'knowledge') {\n <button\n type=\"submit\"\n mat-button\n color=\"primary\">\n Save\n </button>\n <button\n type=\"button\"\n mat-button\n (click)=\"openHistory()\">\n View History\n </button>\n <button\n type=\"button\"\n mat-button\n [mat-dialog-close]=\"null\">\n Cancel\n </button>\n }\n @if (selectedTab === 'build') {\n <button\n type=\"button\"\n mat-button\n color=\"primary\"\n [mat-dialog-close]=\"undefined\">\n Close\n </button>\n }\n @if (selectedTab === 'build' && semanticDatabase(); as db) {\n @if (db.buildActive()) {\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"db.stopBuild()\">\n Stop Build\n </button>\n } @else {\n @if (db.status(); as buildStatus) {\n <button\n type=\"button\"\n mat-button\n (click)=\"db.build()\">\n {{ buildStatus.detail.generatedAt ? 'Rebuild' : 'Build' }}\n </button>\n }\n }\n }\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n", styles: [":host{display:block}\n"] }]
|
|
4051
4196
|
}], ctorParameters: () => [] });
|
|
4052
4197
|
|
|
4053
|
-
// The reports listing — an fs-
|
|
4054
|
-
// pattern (a ':id' token → the report id; a leading
|
|
4055
|
-
// otherwise it's relative to this component's route).
|
|
4056
|
-
// title, creates the report, then routes to it. The
|
|
4057
|
-
// global Report Settings dialog (AI knowledge document +
|
|
4058
|
-
// build), which lives in this package. No routing is
|
|
4198
|
+
// The reports listing — an fs-tree of folders and the reports filed into them.
|
|
4199
|
+
// Rows link out through a URL pattern (a ':id' token → the report id; a leading
|
|
4200
|
+
// '/' makes it absolute, otherwise it's relative to this component's route).
|
|
4201
|
+
// Create prompts for a title, creates the report, then routes to it. The gear
|
|
4202
|
+
// opens the global Report Settings dialog (AI knowledge document +
|
|
4203
|
+
// semantic-database build), which lives in this package. No routing is
|
|
4204
|
+
// hard-coded here.
|
|
4205
|
+
//
|
|
4206
|
+
// Folders nest; reports are always leaves — canDrop enforces it here, and the
|
|
4207
|
+
// backend enforces it structurally by keeping the tree in its own table. A
|
|
4208
|
+
// report in no folder is a root node, which is where every report sat before
|
|
4209
|
+
// folders existed, so nothing had to be migrated or bucketed as "uncategorized".
|
|
4059
4210
|
//
|
|
4060
4211
|
// Reports are soft-deletable: the row kebab deletes, "Show Deleted" flips the
|
|
4061
|
-
//
|
|
4062
|
-
// that mode
|
|
4063
|
-
//
|
|
4212
|
+
// listing into deleted mode (the backend hides deleted reports otherwise), and
|
|
4213
|
+
// in that mode the kebab offers Restore instead. Folders do NOT take part in
|
|
4214
|
+
// that — a folder is only deletable once it is empty, so it hard deletes and has
|
|
4215
|
+
// nothing to restore. The upshot is that deleted mode keeps the folder structure
|
|
4216
|
+
// on screen with the deleted reports sitting where they belong, so restoring one
|
|
4217
|
+
// puts it back rather than dropping it at the root.
|
|
4064
4218
|
class FsAiReportsComponent {
|
|
4219
|
+
// How deep the tree opens on load. Folders are audience buckets, not a
|
|
4220
|
+
// filesystem, so there is nothing worth hiding behind a closed chevron.
|
|
4221
|
+
static EXPAND_LEVEL = 10;
|
|
4065
4222
|
// API base for the reports endpoints; override to mount elsewhere.
|
|
4066
4223
|
basePath = input('reports');
|
|
4067
4224
|
// Where a row links to; ':id' is replaced with the report id. Default takes
|
|
@@ -4069,27 +4226,44 @@ class FsAiReportsComponent {
|
|
|
4069
4226
|
openPattern = input(':id');
|
|
4070
4227
|
// Where a freshly-created report routes to; same ':id' pattern rules.
|
|
4071
4228
|
createPattern = input(':id');
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4229
|
+
// Null until the first load resolves — that is what gates *fsSkeleton.
|
|
4230
|
+
nodes = signal(null);
|
|
4231
|
+
treeConfig;
|
|
4232
|
+
filterConfig;
|
|
4233
|
+
// Template-side handle on the enum, so a row can tell a folder from a report.
|
|
4234
|
+
nodeType = ReportNodeType;
|
|
4235
|
+
_tree = viewChild(FsTreeComponent);
|
|
4077
4236
|
_reportData = inject(ReportData);
|
|
4078
4237
|
_router = inject(Router);
|
|
4079
4238
|
_route = inject(ActivatedRoute);
|
|
4080
4239
|
_prompt = inject(FsPrompt);
|
|
4240
|
+
_message = inject(FsMessage);
|
|
4081
4241
|
_dialog = inject(MatDialog);
|
|
4082
4242
|
_destroyRef = inject(DestroyRef);
|
|
4243
|
+
// The filter bar's current query — carries `state` when Show Deleted is on.
|
|
4244
|
+
_query = {};
|
|
4083
4245
|
constructor() {
|
|
4084
4246
|
effect(() => {
|
|
4085
4247
|
this._reportData.basePath = this.basePath();
|
|
4086
4248
|
});
|
|
4087
|
-
|
|
4249
|
+
}
|
|
4250
|
+
ngOnInit() {
|
|
4251
|
+
// Set before the first fetch rather than waiting on the effect above, which
|
|
4252
|
+
// does not run until after the initial change detection pass.
|
|
4253
|
+
this._reportData.basePath = this.basePath();
|
|
4254
|
+
this._initFilterConfig();
|
|
4255
|
+
this._initTreeConfig();
|
|
4256
|
+
this._load();
|
|
4088
4257
|
}
|
|
4089
4258
|
// routerLink commands for a report row (built from openPattern).
|
|
4090
4259
|
openLink(report) {
|
|
4091
4260
|
return this._patternCommands(this.openPattern(), report.id);
|
|
4092
4261
|
}
|
|
4262
|
+
// A deleted report can't be opened — the backend only assembles active ones —
|
|
4263
|
+
// so in Show Deleted mode its name renders as plain text rather than a link.
|
|
4264
|
+
isDeleted(report) {
|
|
4265
|
+
return report?.state === ReportState.Deleted;
|
|
4266
|
+
}
|
|
4093
4267
|
// Prompt for a title, create the report, then route to it (createPattern).
|
|
4094
4268
|
createReport() {
|
|
4095
4269
|
this._prompt.input({
|
|
@@ -4098,27 +4272,12 @@ class FsAiReportsComponent {
|
|
|
4098
4272
|
commitLabel: 'Create',
|
|
4099
4273
|
required: true,
|
|
4100
4274
|
})
|
|
4101
|
-
.pipe(switchMap((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
|
|
4275
|
+
.pipe(switchMap$1((name) => this._reportData.create(name)), takeUntilDestroyed(this._destroyRef))
|
|
4102
4276
|
.subscribe((report) => {
|
|
4103
4277
|
const pattern = this.createPattern();
|
|
4104
4278
|
this._router.navigate(this._patternCommands(pattern, report.id), pattern.startsWith('/') ? {} : { relativeTo: this._route });
|
|
4105
4279
|
});
|
|
4106
4280
|
}
|
|
4107
|
-
// Turn a ':id' pattern into router commands. Relative unless it starts
|
|
4108
|
-
// with '/', in which case the first segment carries the leading slash.
|
|
4109
|
-
_patternCommands(pattern, id) {
|
|
4110
|
-
const absolute = pattern.startsWith('/');
|
|
4111
|
-
const segments = pattern.split('/')
|
|
4112
|
-
.filter((segment) => segment.length > 0)
|
|
4113
|
-
.map((segment) => (segment === ':id' ? id : segment));
|
|
4114
|
-
if (!segments.length) {
|
|
4115
|
-
return [id];
|
|
4116
|
-
}
|
|
4117
|
-
if (absolute) {
|
|
4118
|
-
segments[0] = `/${segments[0]}`;
|
|
4119
|
-
}
|
|
4120
|
-
return segments;
|
|
4121
|
-
}
|
|
4122
4281
|
// The global Report Settings dialog: the AI knowledge document and the
|
|
4123
4282
|
// semantic-database build. A running build streams into the FsProcess dock,
|
|
4124
4283
|
// which survives the dialog closing.
|
|
@@ -4130,66 +4289,315 @@ class FsAiReportsComponent {
|
|
|
4130
4289
|
autoFocus: false,
|
|
4131
4290
|
});
|
|
4132
4291
|
}
|
|
4133
|
-
|
|
4134
|
-
this.
|
|
4135
|
-
|
|
4136
|
-
|
|
4292
|
+
_initFilterConfig() {
|
|
4293
|
+
this.filterConfig = {
|
|
4294
|
+
items: [
|
|
4295
|
+
{
|
|
4296
|
+
name: 'keyword',
|
|
4297
|
+
type: ItemType.Keyword,
|
|
4298
|
+
label: 'Search',
|
|
4299
|
+
},
|
|
4300
|
+
{
|
|
4301
|
+
name: 'state',
|
|
4302
|
+
type: ItemType.Select,
|
|
4303
|
+
label: 'Status',
|
|
4304
|
+
multiple: true,
|
|
4305
|
+
values: () => ReportStates,
|
|
4306
|
+
isolate: { label: 'Show Deleted', value: ReportState.Deleted },
|
|
4307
|
+
},
|
|
4308
|
+
],
|
|
4137
4309
|
actions: [
|
|
4138
4310
|
{
|
|
4139
|
-
|
|
4311
|
+
icon: 'unfold_more',
|
|
4312
|
+
tooltip: 'Expand all',
|
|
4313
|
+
primary: false,
|
|
4314
|
+
click: () => this._tree()?.expandAll(),
|
|
4315
|
+
},
|
|
4316
|
+
{
|
|
4317
|
+
icon: 'unfold_less',
|
|
4318
|
+
tooltip: 'Collapse all',
|
|
4319
|
+
primary: false,
|
|
4320
|
+
click: () => this._tree()?.collapseAll(),
|
|
4321
|
+
},
|
|
4322
|
+
{
|
|
4140
4323
|
icon: 'settings',
|
|
4141
4324
|
tooltip: 'Report settings',
|
|
4142
4325
|
primary: false,
|
|
4143
4326
|
click: () => this.openSettings(),
|
|
4144
4327
|
},
|
|
4328
|
+
{
|
|
4329
|
+
label: 'New Folder',
|
|
4330
|
+
primary: false,
|
|
4331
|
+
click: () => this._createFolder(null),
|
|
4332
|
+
},
|
|
4145
4333
|
{
|
|
4146
4334
|
label: 'Create',
|
|
4147
4335
|
primary: true,
|
|
4148
4336
|
click: () => this.createReport(),
|
|
4149
4337
|
},
|
|
4150
4338
|
],
|
|
4151
|
-
|
|
4339
|
+
// The two items are answered in different places and both have to run.
|
|
4340
|
+
// The keyword drives the tree's own search, which is the whole point of
|
|
4341
|
+
// searching a tree — it reveals a match nested inside collapsed folders
|
|
4342
|
+
// rather than only filtering what is already on screen. `state` is the
|
|
4343
|
+
// Show Deleted toggle, and only the server can answer that.
|
|
4344
|
+
change: (query) => {
|
|
4345
|
+
const stateChanged = String(query?.state ?? '') !== String(this._query.state ?? '');
|
|
4346
|
+
this._query = { ...query };
|
|
4347
|
+
this._search(query?.keyword);
|
|
4348
|
+
if (stateChanged) {
|
|
4349
|
+
this._load();
|
|
4350
|
+
}
|
|
4351
|
+
},
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
_initTreeConfig() {
|
|
4355
|
+
this.treeConfig = {
|
|
4356
|
+
data: [],
|
|
4357
|
+
childrenName: 'children',
|
|
4358
|
+
expandLevel: FsAiReportsComponent.EXPAND_LEVEL,
|
|
4359
|
+
draggable: true,
|
|
4360
|
+
// The whole row is the grip and the handle stays hidden; a press only
|
|
4361
|
+
// becomes a drag once the pointer moves, so the kebab, the chevron and the
|
|
4362
|
+
// report link all stay clickable.
|
|
4363
|
+
dragMode: TreeDragMode.Node,
|
|
4364
|
+
// canDrop is asked about the node being DRAGGED, so only `toParent`
|
|
4365
|
+
// describes the destination — reading node.level/node.parent here would
|
|
4366
|
+
// describe where it came from. A report can never be a parent because its
|
|
4367
|
+
// type says so, and that single test is the whole "folders nest, reports
|
|
4368
|
+
// are leaves" rule on this side.
|
|
4369
|
+
canDrop: (node, fromParent, toParent) => {
|
|
4370
|
+
return !toParent || toParent.data?.type === ReportNodeType.Folder;
|
|
4371
|
+
},
|
|
4372
|
+
changeReorder: (event) => this._move(event),
|
|
4373
|
+
nodeClass: (node) => node.data?.type,
|
|
4374
|
+
// Match on the name only. The library's default matcher stringifies the
|
|
4375
|
+
// whole node, so it would hit ids, the literal type word, and everything
|
|
4376
|
+
// else riding on the row.
|
|
4377
|
+
filterItem: (node, query) => {
|
|
4378
|
+
return String(node.data?.name ?? '')
|
|
4379
|
+
.toLowerCase()
|
|
4380
|
+
.includes(String(query ?? '').toLowerCase());
|
|
4381
|
+
},
|
|
4382
|
+
actions: [
|
|
4152
4383
|
{
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4384
|
+
type: TreeActionType.Menu,
|
|
4385
|
+
icon: 'more_vert',
|
|
4386
|
+
items: [
|
|
4387
|
+
{
|
|
4388
|
+
label: 'Rename',
|
|
4389
|
+
show: (node) => !this._isDeletedNode(node),
|
|
4390
|
+
click: (node) => this._rename(node),
|
|
4391
|
+
},
|
|
4392
|
+
{
|
|
4393
|
+
label: 'New Folder Inside',
|
|
4394
|
+
show: (node) => this._isFolderNode(node),
|
|
4395
|
+
click: (node) => this._createFolder(node.data?.id),
|
|
4396
|
+
},
|
|
4397
|
+
{
|
|
4398
|
+
label: 'Delete',
|
|
4399
|
+
show: (node) => !this._isDeletedNode(node),
|
|
4400
|
+
click: (node) => this._delete(node),
|
|
4401
|
+
},
|
|
4402
|
+
// Folders never reach this: they hard delete, so a deleted folder
|
|
4403
|
+
// does not exist to be restored.
|
|
4404
|
+
{
|
|
4405
|
+
label: 'Restore',
|
|
4406
|
+
show: (node) => this._isDeletedNode(node),
|
|
4407
|
+
click: (node) => this._restore(node),
|
|
4408
|
+
},
|
|
4409
|
+
],
|
|
4160
4410
|
},
|
|
4161
4411
|
],
|
|
4162
|
-
// `filter: true` makes fs-list render the Show Deleted toggle and send
|
|
4163
|
-
// `query` with the fetch; in that mode it also swaps the row's Delete
|
|
4164
|
-
// action for Restore.
|
|
4165
|
-
restore: {
|
|
4166
|
-
query: { state: ReportState.Deleted },
|
|
4167
|
-
filter: true,
|
|
4168
|
-
filterLabel: 'Show Deleted',
|
|
4169
|
-
menuLabel: 'Restore',
|
|
4170
|
-
reload: true,
|
|
4171
|
-
click: (report) => this._reportData.undelete(report.id),
|
|
4172
|
-
},
|
|
4173
|
-
fetch: (query) => {
|
|
4174
|
-
return this._reportData.reports(query)
|
|
4175
|
-
.pipe(map((reports) => ({ data: reports ?? [] })));
|
|
4176
|
-
},
|
|
4177
4412
|
};
|
|
4178
4413
|
}
|
|
4414
|
+
_isFolderNode(node) {
|
|
4415
|
+
return node?.data?.type === ReportNodeType.Folder;
|
|
4416
|
+
}
|
|
4417
|
+
_isDeletedNode(node) {
|
|
4418
|
+
return this.isDeleted(node?.data?.report);
|
|
4419
|
+
}
|
|
4420
|
+
// Drive the tree's built-in search: it collapses everything, hides every node,
|
|
4421
|
+
// then unhides each match and walks its ancestors open, so a report several
|
|
4422
|
+
// folders deep surfaces with its path expanded. Clearing does NOT undo that
|
|
4423
|
+
// initial collapse, so the tree is re-opened here — otherwise the listing is
|
|
4424
|
+
// left fully collapsed the moment somebody empties the search box.
|
|
4425
|
+
_search(keyword) {
|
|
4426
|
+
const tree = this._tree();
|
|
4427
|
+
if (!tree) {
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
if (keyword) {
|
|
4431
|
+
tree.search(keyword);
|
|
4432
|
+
return;
|
|
4433
|
+
}
|
|
4434
|
+
tree.clearSearch();
|
|
4435
|
+
tree.expandAll();
|
|
4436
|
+
}
|
|
4437
|
+
_load() {
|
|
4438
|
+
this._reportData.tree(this._query)
|
|
4439
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
4440
|
+
.subscribe((tree) => {
|
|
4441
|
+
const nodes = this._buildNodes(tree);
|
|
4442
|
+
this.nodes.set(nodes);
|
|
4443
|
+
// The tree reads `config` once, in its own ngOnInit. The first load
|
|
4444
|
+
// lands before it exists, so the data has to arrive through the config;
|
|
4445
|
+
// every later one has to go through the instance or nothing changes.
|
|
4446
|
+
this.treeConfig = { ...this.treeConfig, data: nodes };
|
|
4447
|
+
this._tree()?.setData(nodes);
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
// Flat API rows → the nested shape fs-tree renders. Folders come before
|
|
4451
|
+
// reports at every level, each already name-ordered by the backend.
|
|
4452
|
+
_buildNodes(tree) {
|
|
4453
|
+
const folders = tree?.folders ?? [];
|
|
4454
|
+
const reports = tree?.reports ?? [];
|
|
4455
|
+
const build = (parentId) => {
|
|
4456
|
+
const folderNodes = folders
|
|
4457
|
+
.filter((folder) => (folder.parentReportFolderId ?? null) === parentId)
|
|
4458
|
+
.map((folder) => ({
|
|
4459
|
+
key: `f:${folder.id}`,
|
|
4460
|
+
id: folder.id,
|
|
4461
|
+
name: folder.name,
|
|
4462
|
+
type: ReportNodeType.Folder,
|
|
4463
|
+
children: build(folder.id),
|
|
4464
|
+
}));
|
|
4465
|
+
const reportNodes = reports
|
|
4466
|
+
.filter((report) => (report.reportFolderId ?? null) === parentId)
|
|
4467
|
+
.map((report) => ({
|
|
4468
|
+
key: `r:${report.id}`,
|
|
4469
|
+
id: report.id,
|
|
4470
|
+
name: report.name,
|
|
4471
|
+
type: ReportNodeType.Report,
|
|
4472
|
+
children: [],
|
|
4473
|
+
report,
|
|
4474
|
+
}));
|
|
4475
|
+
return [...folderNodes, ...reportNodes];
|
|
4476
|
+
};
|
|
4477
|
+
return build(null);
|
|
4478
|
+
}
|
|
4479
|
+
// A drop moved a node under a new parent, or out to the root. The tree has
|
|
4480
|
+
// already moved it on screen, so both outcomes reload: a success to pick up
|
|
4481
|
+
// the server's ordering, a failure to put the row back where it really is.
|
|
4482
|
+
_move(event) {
|
|
4483
|
+
const node = event.node?.data;
|
|
4484
|
+
if (!node) {
|
|
4485
|
+
return;
|
|
4486
|
+
}
|
|
4487
|
+
const parentReportFolderId = event.toParent?.data?.id ?? null;
|
|
4488
|
+
if (event.toParent && !event.toParent.isExpanded()) {
|
|
4489
|
+
event.toParent.expand();
|
|
4490
|
+
}
|
|
4491
|
+
const save$ = node.type === ReportNodeType.Folder
|
|
4492
|
+
? this._reportData.updateFolder(node.id, { parentReportFolderId })
|
|
4493
|
+
: this._reportData.moveReport(node.id, parentReportFolderId);
|
|
4494
|
+
save$
|
|
4495
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
4496
|
+
.subscribe({
|
|
4497
|
+
next: () => {
|
|
4498
|
+
this._message.success('Saved changes');
|
|
4499
|
+
this._load();
|
|
4500
|
+
},
|
|
4501
|
+
error: () => this._load(),
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
4504
|
+
_createFolder(parentReportFolderId) {
|
|
4505
|
+
this._prompt.input({
|
|
4506
|
+
title: 'New Folder',
|
|
4507
|
+
label: 'Name',
|
|
4508
|
+
commitLabel: 'Create',
|
|
4509
|
+
required: true,
|
|
4510
|
+
})
|
|
4511
|
+
.pipe(switchMap$1((name) => this._reportData.createFolder(name, parentReportFolderId)), takeUntilDestroyed(this._destroyRef))
|
|
4512
|
+
.subscribe(() => {
|
|
4513
|
+
this._message.success('Created folder');
|
|
4514
|
+
this._load();
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
_rename(node) {
|
|
4518
|
+
const data = node?.data;
|
|
4519
|
+
if (!data) {
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
const folder = data.type === ReportNodeType.Folder;
|
|
4523
|
+
this._prompt.input({
|
|
4524
|
+
title: folder ? 'Rename Folder' : 'Rename Report',
|
|
4525
|
+
label: 'Name',
|
|
4526
|
+
default: data.name,
|
|
4527
|
+
commitLabel: 'Save',
|
|
4528
|
+
required: true,
|
|
4529
|
+
})
|
|
4530
|
+
.pipe(switchMap$1((name) => (folder
|
|
4531
|
+
? this._reportData.updateFolder(data.id, { name })
|
|
4532
|
+
: this._reportData.rename(data.id, name))), takeUntilDestroyed(this._destroyRef))
|
|
4533
|
+
.subscribe(() => {
|
|
4534
|
+
this._message.success('Saved changes');
|
|
4535
|
+
this._load();
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
// Deleting a folder is refused server-side while it still holds a sub-folder
|
|
4539
|
+
// or an active report, and the message says which — so the confirm here warns
|
|
4540
|
+
// that a folder delete is permanent rather than trying to restate the rule.
|
|
4541
|
+
_delete(node) {
|
|
4542
|
+
const data = node?.data;
|
|
4543
|
+
if (!data) {
|
|
4544
|
+
return;
|
|
4545
|
+
}
|
|
4546
|
+
const folder = data.type === ReportNodeType.Folder;
|
|
4547
|
+
this._prompt.confirm({
|
|
4548
|
+
title: 'Confirm',
|
|
4549
|
+
template: folder
|
|
4550
|
+
? `Are you sure you would like to delete the folder "${data.name}"? This cannot be undone.`
|
|
4551
|
+
: 'Are you sure you would like to delete this report?',
|
|
4552
|
+
})
|
|
4553
|
+
.pipe(switchMap$1(() => (folder
|
|
4554
|
+
? this._reportData.deleteFolder(data.id)
|
|
4555
|
+
: this._reportData.delete(data.id))), takeUntilDestroyed(this._destroyRef))
|
|
4556
|
+
.subscribe(() => {
|
|
4557
|
+
this._message.success(folder ? 'Deleted folder' : 'Deleted report');
|
|
4558
|
+
this._load();
|
|
4559
|
+
});
|
|
4560
|
+
}
|
|
4561
|
+
_restore(node) {
|
|
4562
|
+
const data = node?.data;
|
|
4563
|
+
if (!data) {
|
|
4564
|
+
return;
|
|
4565
|
+
}
|
|
4566
|
+
this._reportData.undelete(data.id)
|
|
4567
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
4568
|
+
.subscribe(() => {
|
|
4569
|
+
this._message.success('Restored report');
|
|
4570
|
+
this._load();
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
// Turn a ':id' pattern into router commands. Relative unless it starts
|
|
4574
|
+
// with '/', in which case the first segment carries the leading slash.
|
|
4575
|
+
_patternCommands(pattern, id) {
|
|
4576
|
+
const absolute = pattern.startsWith('/');
|
|
4577
|
+
const segments = pattern.split('/')
|
|
4578
|
+
.filter((segment) => segment.length > 0)
|
|
4579
|
+
.map((segment) => (segment === ':id' ? id : segment));
|
|
4580
|
+
if (!segments.length) {
|
|
4581
|
+
return [id];
|
|
4582
|
+
}
|
|
4583
|
+
if (absolute) {
|
|
4584
|
+
segments[0] = `/${segments[0]}`;
|
|
4585
|
+
}
|
|
4586
|
+
return segments;
|
|
4587
|
+
}
|
|
4179
4588
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4180
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "
|
|
4589
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportsComponent, isStandalone: true, selector: "fs-ai-reports", inputs: { basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, openPattern: { classPropertyName: "openPattern", publicName: "openPattern", isSignal: true, isRequired: false, transformFunction: null }, createPattern: { classPropertyName: "createPattern", publicName: "createPattern", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "_tree", first: true, predicate: FsTreeComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div class=\"fs-row.gap-sm.align-center node-row\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder) {\n <span class=\"node-name\">{{ data.name }}</span>\n } @else if (isDeleted(data.report)) {\n <!-- A deleted report can't be opened (the backend only assembles\n active ones), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4$1.FsSkeletonContentDirective, selector: "[fsSkeleton]", inputs: ["fsSkeleton", "fsSkeletonPattern"] }, { kind: "ngmodule", type: FsTreeModule }, { kind: "component", type: i3$1.FsTreeComponent, selector: "fs-tree", inputs: ["config"] }, { kind: "directive", type: i3$1.FsTreeNodeDirective, selector: "[fsTreeNode]", inputs: ["class"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4181
4590
|
}
|
|
4182
4591
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportsComponent, decorators: [{
|
|
4183
4592
|
type: Component,
|
|
4184
4593
|
args: [{ selector: 'fs-ai-reports', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
|
|
4185
|
-
|
|
4186
|
-
|
|
4594
|
+
FsFilterModule,
|
|
4595
|
+
FsSkeletonModule,
|
|
4596
|
+
FsTreeModule,
|
|
4597
|
+
MatIconModule,
|
|
4187
4598
|
RouterLink,
|
|
4188
|
-
], template: "<fs-
|
|
4189
|
-
}], ctorParameters: () => []
|
|
4190
|
-
type: ViewChild,
|
|
4191
|
-
args: [FsListComponent]
|
|
4192
|
-
}] } });
|
|
4599
|
+
], template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div class=\"fs-row.gap-sm.align-center node-row\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder) {\n <span class=\"node-name\">{{ data.name }}</span>\n } @else if (isDeleted(data.report)) {\n <!-- A deleted report can't be opened (the backend only assembles\n active ones), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"] }]
|
|
4600
|
+
}], ctorParameters: () => [] });
|
|
4193
4601
|
|
|
4194
4602
|
// fs-filter query key for the runtime Frequency control (a render-layer bucket
|
|
4195
4603
|
// override, not a filter group — so it's read straight off the query).
|
|
@@ -4215,6 +4623,9 @@ class FsAiReportComponent {
|
|
|
4215
4623
|
reportFilterConfig;
|
|
4216
4624
|
_split;
|
|
4217
4625
|
_chatPanel;
|
|
4626
|
+
// Only the canvas knows which page is on screen, and a page-scoped export
|
|
4627
|
+
// has to ask it.
|
|
4628
|
+
_canvas;
|
|
4218
4629
|
_reportData = inject(ReportData);
|
|
4219
4630
|
_filterState = inject(ReportFilterStateService);
|
|
4220
4631
|
_exportCollector = inject(ReportExportCollectorService);
|
|
@@ -4318,6 +4729,19 @@ class FsAiReportComponent {
|
|
|
4318
4729
|
this._process.run('Exporting PDF', this._exportCollector.collect(report)
|
|
4319
4730
|
.pipe(switchMap((collected) => from(this._pdfService.export(collected)))));
|
|
4320
4731
|
}
|
|
4732
|
+
// The raw data behind the whole report in one action: the server exports every
|
|
4733
|
+
// component's full filtered dataset and zips one CSV per component, foldered
|
|
4734
|
+
// by page. `thisPage` narrows it to the page the canvas is showing. Unlike the
|
|
4735
|
+
// PDF/PowerPoint exports this is entirely server-side — nothing is collected
|
|
4736
|
+
// in the browser, so it is not capped by what a slide can hold.
|
|
4737
|
+
exportData(thisPage = false) {
|
|
4738
|
+
const report = this.report;
|
|
4739
|
+
if (!report) {
|
|
4740
|
+
return;
|
|
4741
|
+
}
|
|
4742
|
+
const pageId = thisPage ? this._canvas?.activePage?.id ?? null : null;
|
|
4743
|
+
this._process.download('Exporting data', this._reportData.exportReport(report.id, this._filterState.resolveForReport(report, pageId), pageId));
|
|
4744
|
+
}
|
|
4321
4745
|
// React to each chat turn (fs-chat handles the request + persistence); we
|
|
4322
4746
|
// only care about the report side effects this context's backend flags.
|
|
4323
4747
|
onChatResponse(response) {
|
|
@@ -4393,7 +4817,10 @@ class FsAiReportComponent {
|
|
|
4393
4817
|
// rebuilt config) whenever the report-level filter set changes.
|
|
4394
4818
|
get reportFilterKey() {
|
|
4395
4819
|
return [
|
|
4396
|
-
|
|
4820
|
+
// The stored default is part of the signature: saving a new default in report
|
|
4821
|
+
// settings rebuilds the config, but without a key change fs-filter keeps
|
|
4822
|
+
// rendering the values it was created with until a full page reload.
|
|
4823
|
+
...this._reportGroups().map((group) => `${group.id}:${group.level}:${group.label}:${defaultSignature(group)}`),
|
|
4397
4824
|
this._hasTimeSeriesChart() ? 'frequency' : '',
|
|
4398
4825
|
].join('|');
|
|
4399
4826
|
}
|
|
@@ -4443,8 +4870,8 @@ class FsAiReportComponent {
|
|
|
4443
4870
|
ReportPptxService,
|
|
4444
4871
|
ReportPdfService,
|
|
4445
4872
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
4446
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
4447
|
-
], 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 <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <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 <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:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: 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", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$
|
|
4873
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-BYmAhi-N.mjs').then((module) => module.default) }),
|
|
4874
|
+
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <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 <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </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 <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:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: 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", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$2.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: i2$5.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 });
|
|
4448
4875
|
}
|
|
4449
4876
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
|
|
4450
4877
|
type: Component,
|
|
@@ -4456,7 +4883,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4456
4883
|
ReportPptxService,
|
|
4457
4884
|
ReportPdfService,
|
|
4458
4885
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
4459
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
4886
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-BYmAhi-N.mjs').then((module) => module.default) }),
|
|
4460
4887
|
], imports: [
|
|
4461
4888
|
FsFilterModule,
|
|
4462
4889
|
FsMenuModule,
|
|
@@ -4465,13 +4892,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4465
4892
|
MatIconModule,
|
|
4466
4893
|
MatTooltipModule,
|
|
4467
4894
|
ReportCanvasComponent,
|
|
4468
|
-
], template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <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 <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:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
|
|
4895
|
+
], template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report?.name }}</h2>\n\n @if (report) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig\">\n </fs-filter>\n }\n }\n\n <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 <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </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 <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:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
|
|
4469
4896
|
}], ctorParameters: () => [], propDecorators: { _split: [{
|
|
4470
4897
|
type: ViewChild,
|
|
4471
4898
|
args: ['split', { static: true }]
|
|
4472
4899
|
}], _chatPanel: [{
|
|
4473
4900
|
type: ViewChild,
|
|
4474
4901
|
args: ['chatPanel', { static: true, read: ElementRef }]
|
|
4902
|
+
}], _canvas: [{
|
|
4903
|
+
type: ViewChild,
|
|
4904
|
+
args: [ReportCanvasComponent]
|
|
4475
4905
|
}] } });
|
|
4476
4906
|
|
|
4477
4907
|
/*
|
|
@@ -4486,5 +4916,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4486
4916
|
* Generated bundle index. Do not edit.
|
|
4487
4917
|
*/
|
|
4488
4918
|
|
|
4489
|
-
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,
|
|
4490
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
4919
|
+
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, ReportNodeType as g, ReportState as h, ReportStates as i };
|
|
4920
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-acZuB8V2.mjs.map
|