@firestitch/report 18.11.0 → 18.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/reports/components/component-grid/component-grid.component.d.ts +29 -0
- package/app/reports/components/component-grid/report-grid-formatter.d.ts +9 -0
- package/app/reports/data/report-grid-source.d.ts +22 -0
- package/app/reports/data/report.data.d.ts +3 -1
- package/app/reports/interfaces/report-chat-viewer-state.interface.d.ts +2 -0
- package/app/reports/interfaces/report-chat-viewer-tile.interface.d.ts +5 -0
- package/app/reports/interfaces/report-grid-config.interface.d.ts +5 -0
- package/app/reports/interfaces/report-grid-line.interface.d.ts +6 -0
- package/app/reports/interfaces/report-grid-params.interface.d.ts +7 -0
- package/app/reports/interfaces/report-grid-report.interface.d.ts +2 -0
- package/app/reports/interfaces/report-grid-session-layout.interface.d.ts +5 -0
- package/app/reports/interfaces/report.interface.d.ts +3 -2
- package/app/reports/report-chat-viewer-state.d.ts +4 -1
- package/app/reports/report-grid-layout.d.ts +4 -0
- package/app/reports/services/report-filter-state.service.d.ts +6 -0
- package/app/reports/views/report/fs-ai-report.component.d.ts +2 -0
- package/esm2022/app/reports/components/component-grid/component-grid.component.mjs +93 -0
- package/esm2022/app/reports/components/component-grid/report-grid-formatter.mjs +94 -0
- package/esm2022/app/reports/components/component-text/component-text.component.mjs +1 -1
- package/esm2022/app/reports/components/report-component/report-component.component.mjs +21 -14
- package/esm2022/app/reports/data/report-grid-source.mjs +105 -0
- package/esm2022/app/reports/data/report.data.mjs +13 -2
- package/esm2022/app/reports/interfaces/report-chat-viewer-state.interface.mjs +1 -1
- package/esm2022/app/reports/interfaces/report-chat-viewer-tile.interface.mjs +2 -0
- package/esm2022/app/reports/interfaces/report-grid-config.interface.mjs +2 -0
- package/esm2022/app/reports/interfaces/report-grid-line.interface.mjs +2 -0
- package/esm2022/app/reports/interfaces/report-grid-params.interface.mjs +2 -0
- package/esm2022/app/reports/interfaces/report-grid-report.interface.mjs +2 -0
- package/esm2022/app/reports/interfaces/report-grid-session-layout.interface.mjs +2 -0
- package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
- package/esm2022/app/reports/report-chat-viewer-state.mjs +21 -6
- package/esm2022/app/reports/report-grid-layout.mjs +14 -0
- package/esm2022/app/reports/report-tile.mjs +5 -5
- package/esm2022/app/reports/services/report-filter-state.service.mjs +77 -11
- package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +36 -13
- package/fesm2022/{firestitch-report-echarts-DKXlgu1D.mjs → firestitch-report-echarts-CoaGXKET.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-DKXlgu1D.mjs.map → firestitch-report-echarts-CoaGXKET.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-Bnv62suw.mjs → firestitch-report-firestitch-report-CHPA7sla.mjs} +457 -52
- package/fesm2022/firestitch-report-firestitch-report-CHPA7sla.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +4 -2
- package/fesm2022/firestitch-report-firestitch-report-Bnv62suw.mjs.map +0 -1
|
@@ -27,14 +27,16 @@ import * as i6$1 from '@firestitch/message';
|
|
|
27
27
|
import { FsMessage, FsMessageModule } from '@firestitch/message';
|
|
28
28
|
import * as i3$1 from '@firestitch/zoom-pan';
|
|
29
29
|
import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
|
|
30
|
-
import { of, forkJoin, Subject, defer, merge, map as map$1, switchMap as switchMap$1, timer, catchError as catchError$1, filter as filter$1, tap as tap$1, distinctUntilChanged,
|
|
31
|
-
import { FsApi,
|
|
30
|
+
import { of, forkJoin, Subject, defer, merge, map as map$1, switchMap as switchMap$1, timer, catchError as catchError$1, filter as filter$1, mergeMap, EMPTY, concatWith, throwError, tap as tap$1, distinctUntilChanged, from, debounceTime, BehaviorSubject } from 'rxjs';
|
|
31
|
+
import { FsApi, RequestMethod, StreamEventType, DisplayApiError } from '@firestitch/api';
|
|
32
32
|
import { parseISO, isValid, addDays, min, max, startOfDay, isLastDayOfMonth, endOfMonth, addMonths, addYears, addQuarters, addWeeks, formatISO, format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek } from 'date-fns';
|
|
33
33
|
import { toZonedTime } from 'date-fns-tz';
|
|
34
|
-
import {
|
|
34
|
+
import { canonicalGridLayout, GridFormat, GridUnits, GridFieldType, FsGridComponent } from '@firestitch/grid';
|
|
35
|
+
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
|
35
36
|
import { MatCheckbox } from '@angular/material/checkbox';
|
|
36
37
|
import * as i3 from '@firestitch/dialog';
|
|
37
38
|
import { FsDialogModule } from '@firestitch/dialog';
|
|
39
|
+
import { HttpErrorResponse, HttpContext } from '@angular/common/http';
|
|
38
40
|
import { NgTemplateOutlet, DOCUMENT } from '@angular/common';
|
|
39
41
|
import * as i1 from '@firestitch/list';
|
|
40
42
|
import { FsListComponent, PaginationStrategy, FsListModule } from '@firestitch/list';
|
|
@@ -55,7 +57,6 @@ import * as i3$2 from '@firestitch/mask';
|
|
|
55
57
|
import { FsMaskModule } from '@firestitch/mask';
|
|
56
58
|
import * as i6 from '@firestitch/tabs';
|
|
57
59
|
import { FsTabsModule } from '@firestitch/tabs';
|
|
58
|
-
import { HttpContext } from '@angular/common/http';
|
|
59
60
|
import * as i9 from '@firestitch/datepicker';
|
|
60
61
|
import { FsDatePickerModule, FS_DATEPICKER_CONFIG_DEFAULT, FS_DATEPICKER_CONFIG } from '@firestitch/datepicker';
|
|
61
62
|
import * as i4 from '@firestitch/skeleton';
|
|
@@ -337,6 +338,17 @@ class ReportData {
|
|
|
337
338
|
...config,
|
|
338
339
|
});
|
|
339
340
|
}
|
|
341
|
+
// One grid tile's read (ReportGridSource): the same route as componentData,
|
|
342
|
+
// its body's `grid` the grid's request. The answer streams as
|
|
343
|
+
// newline-delimited JSON, one event per line: {report} first, then
|
|
344
|
+
// {grid: part} for each part of the read, a heartbeat {at} between
|
|
345
|
+
// statements, and the envelope with the read's summary last. The body's
|
|
346
|
+
// filters and state narrow the rows as they narrow any tile's; the grid's
|
|
347
|
+
// own column filters travel inside grid.layout. `isExport` is
|
|
348
|
+
// componentData's.
|
|
349
|
+
componentGrid(reportId, componentId, filters, state, grid, isExport = false, config = {}) {
|
|
350
|
+
return this._api.stream(RequestMethod.Post, this._path(`${reportId}/components/${componentId}/data`), { filters, state, export: isExport, grid }, config);
|
|
351
|
+
}
|
|
340
352
|
// The numbers of a page's values — or the master's, for the header and
|
|
341
353
|
// footer — in one request: a row per value that page reads, shown by some
|
|
342
354
|
// text or read by a formula it shows (ReportPageValue). `filters` carries
|
|
@@ -4939,10 +4951,10 @@ function selectQueryValues(raw) {
|
|
|
4939
4951
|
// read of the data. The list is positive, so a type added later (or one this
|
|
4940
4952
|
// viewer does not know) is never taken for a tile. The server's twin is
|
|
4941
4953
|
// ReportComponentModel::isTile().
|
|
4942
|
-
const TILE_TYPES = ['chart', 'list', 'kpi'];
|
|
4943
|
-
// Whether a component is a tile: a chart, a list or a KPI. Only a
|
|
4944
|
-
// drawn as one, fetched, exported as a file or counted among a filter
|
|
4945
|
-
// tiles; every loop over a report's tiles reads this.
|
|
4954
|
+
const TILE_TYPES = ['chart', 'list', 'grid', 'kpi'];
|
|
4955
|
+
// Whether a component is a tile: a chart, a list, a grid or a KPI. Only a
|
|
4956
|
+
// tile is drawn as one, fetched, exported as a file or counted among a filter
|
|
4957
|
+
// group's tiles; every loop over a report's tiles reads this.
|
|
4946
4958
|
function isTile(component) {
|
|
4947
4959
|
return !!component && TILE_TYPES.includes(component.type);
|
|
4948
4960
|
}
|
|
@@ -5954,6 +5966,19 @@ function listPriorAlias(outputs, alias, output) {
|
|
|
5954
5966
|
: null;
|
|
5955
5967
|
}
|
|
5956
5968
|
|
|
5969
|
+
// A table tile's layouts: the one its config authors, and the canonical key
|
|
5970
|
+
// two layouts are compared by.
|
|
5971
|
+
const NO_LAYOUT = {};
|
|
5972
|
+
// The layout a table tile's config authors, or none.
|
|
5973
|
+
function authoredGridLayout(component) {
|
|
5974
|
+
return component.config?.layout ?? NO_LAYOUT;
|
|
5975
|
+
}
|
|
5976
|
+
// A layout's canonical key: two layouts that draw the same table have the
|
|
5977
|
+
// same key, whatever order or defaults they were written with.
|
|
5978
|
+
function gridLayoutKey(layout) {
|
|
5979
|
+
return JSON.stringify(canonicalGridLayout(layout));
|
|
5980
|
+
}
|
|
5981
|
+
|
|
5957
5982
|
// Whether two stored filter-group defaults are the same. Compared on the fields a
|
|
5958
5983
|
// default can actually carry, so key order in the payload can't read as a change.
|
|
5959
5984
|
function sameDefault(a, b) {
|
|
@@ -6054,11 +6079,11 @@ function emptyTemporaryState() {
|
|
|
6054
6079
|
//
|
|
6055
6080
|
// It is the one place the viewer's state lives while the report is open: the
|
|
6056
6081
|
// group values, the group-by grain, the match, list sorts, searches and hidden
|
|
6057
|
-
// columns, the page
|
|
6058
|
-
// the browser URL. It outlives a page switch (the canvas
|
|
6059
|
-
// this service) and goes with the report view: a reload
|
|
6060
|
-
// which opens on the report's authored defaults.
|
|
6061
|
-
// change of it.
|
|
6082
|
+
// columns, the page, each chart's drill path and each table's layout. None of
|
|
6083
|
+
// it is ever written to the browser URL. It outlives a page switch (the canvas
|
|
6084
|
+
// changes the page, not this service) and goes with the report view: a reload
|
|
6085
|
+
// starts a new service, which opens on the report's authored defaults.
|
|
6086
|
+
// changes() fires on every change of it.
|
|
6062
6087
|
//
|
|
6063
6088
|
// The durable part reads as one document: snapshot() writes it, and
|
|
6064
6089
|
// differsFromDefaults says whether the screen still shows the report's
|
|
@@ -6068,7 +6093,8 @@ function emptyTemporaryState() {
|
|
|
6068
6093
|
//
|
|
6069
6094
|
// Session-only, never in the snapshot: per-filter enable/disable toggles from
|
|
6070
6095
|
// a component's filter menu (the `enabled` column is the authored default; a
|
|
6071
|
-
// viewer's toggle shouldn't write to the report)
|
|
6096
|
+
// viewer's toggle shouldn't write to the report), and each table's layout,
|
|
6097
|
+
// which fs-grid sends with the table's own reads.
|
|
6072
6098
|
class ReportFilterStateService {
|
|
6073
6099
|
// The open report's effective zone (resolveReportTimezone). Relative presets
|
|
6074
6100
|
// resolve against today in it, and date cells render in it.
|
|
@@ -6181,6 +6207,11 @@ class ReportFilterStateService {
|
|
|
6181
6207
|
_drills = signal(new Map());
|
|
6182
6208
|
// Emits the id of the chart whose drill path changed.
|
|
6183
6209
|
_drillChanged$ = new Subject();
|
|
6210
|
+
// The layout a viewer put each table tile in, by component id, while it
|
|
6211
|
+
// differs from the tile's authored layout, with the authored layout it was
|
|
6212
|
+
// changed from (ReportGridSessionLayout.base). A table showing its authored
|
|
6213
|
+
// layout has no entry, and only a table has one.
|
|
6214
|
+
_gridLayouts = signal(new Map());
|
|
6184
6215
|
// The temporary layer: kept apart from everything above, so snapshot()
|
|
6185
6216
|
// and differsFromDefaults never read it.
|
|
6186
6217
|
_temporary = signal(emptyTemporaryState());
|
|
@@ -6487,6 +6518,45 @@ class ReportFilterStateService {
|
|
|
6487
6518
|
this._stateChanged$.next();
|
|
6488
6519
|
return true;
|
|
6489
6520
|
}
|
|
6521
|
+
// The layout a table tile is on screen in: the viewer's own while they
|
|
6522
|
+
// changed it from the layout authored now, else the authored one. It reads
|
|
6523
|
+
// a signal, so a tile that shows it redraws when either moves.
|
|
6524
|
+
gridLayout(component) {
|
|
6525
|
+
return this.gridSessionLayout(component) ?? authoredGridLayout(component);
|
|
6526
|
+
}
|
|
6527
|
+
// The viewer's own layout of a table tile while it differs from the layout
|
|
6528
|
+
// authored now, else null: the tile shows its stored layout. The chat
|
|
6529
|
+
// sends it as the tile's layout.
|
|
6530
|
+
gridSessionLayout(component) {
|
|
6531
|
+
const session = this._gridLayouts().get(component.id);
|
|
6532
|
+
return session?.base === gridLayoutKey(authoredGridLayout(component))
|
|
6533
|
+
? session.layout
|
|
6534
|
+
: null;
|
|
6535
|
+
}
|
|
6536
|
+
// Records the layout a viewer put a table tile in (a re-pivot, a sort, a
|
|
6537
|
+
// column filter). One that draws the authored layout clears the entry, so
|
|
6538
|
+
// the tile reads as stored again. fs-grid reads the change itself, so only
|
|
6539
|
+
// changes() hears about it, and only on a real change.
|
|
6540
|
+
setGridLayout(component, layout) {
|
|
6541
|
+
const base = gridLayoutKey(authoredGridLayout(component));
|
|
6542
|
+
const key = gridLayoutKey(layout);
|
|
6543
|
+
const current = this._gridLayouts().get(component.id);
|
|
6544
|
+
const unchanged = key === base
|
|
6545
|
+
? !current
|
|
6546
|
+
: current?.base === base && gridLayoutKey(current.layout) === key;
|
|
6547
|
+
if (component.type !== 'grid' || unchanged) {
|
|
6548
|
+
return;
|
|
6549
|
+
}
|
|
6550
|
+
const next = new Map(this._gridLayouts());
|
|
6551
|
+
if (key === base) {
|
|
6552
|
+
next.delete(component.id);
|
|
6553
|
+
}
|
|
6554
|
+
else {
|
|
6555
|
+
next.set(component.id, { layout, base });
|
|
6556
|
+
}
|
|
6557
|
+
this._gridLayouts.set(next);
|
|
6558
|
+
this._stateChanged$.next();
|
|
6559
|
+
}
|
|
6490
6560
|
// The durable state on screen as one document: the page, every group the
|
|
6491
6561
|
// report has (null when it holds nothing, a cleared default included), the
|
|
6492
6562
|
// match, the grain, and each tile's sort, search, hidden columns and drill
|
|
@@ -6850,6 +6920,7 @@ class ReportFilterStateService {
|
|
|
6850
6920
|
this._listSearches.clear();
|
|
6851
6921
|
this.listHidden.set(new Map());
|
|
6852
6922
|
this._drills.set(new Map());
|
|
6923
|
+
this._gridLayouts.set(new Map());
|
|
6853
6924
|
}
|
|
6854
6925
|
// One chart's drill path; an empty path is the chart's top.
|
|
6855
6926
|
_setDrillPath(componentId, path) {
|
|
@@ -6883,20 +6954,39 @@ class ReportFilterStateService {
|
|
|
6883
6954
|
return this._tiles()
|
|
6884
6955
|
.some((component) => component.id === componentId && component.type === 'chart');
|
|
6885
6956
|
}
|
|
6886
|
-
//
|
|
6887
|
-
//
|
|
6888
|
-
|
|
6889
|
-
|
|
6957
|
+
// Drops each table layout whose tile is gone, is no table any more, or is
|
|
6958
|
+
// authored otherwise than when the viewer changed it.
|
|
6959
|
+
_pruneGridLayouts() {
|
|
6960
|
+
const grids = new Map(this._tiles()
|
|
6961
|
+
.filter((component) => component.type === 'grid')
|
|
6962
|
+
.map((component) => [component.id, component]));
|
|
6963
|
+
const kept = new Map([...this._gridLayouts()]
|
|
6964
|
+
.filter(([componentId, session]) => {
|
|
6965
|
+
const component = grids.get(componentId);
|
|
6966
|
+
return !!component && session.base === gridLayoutKey(authoredGridLayout(component));
|
|
6967
|
+
}));
|
|
6968
|
+
if (kept.size !== this._gridLayouts().size) {
|
|
6969
|
+
this._gridLayouts.set(kept);
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
// Keeps the page, drill and table state to what the report (re)loaded by
|
|
6973
|
+
// init() still has: drill paths on charts it still has, the page while it
|
|
6974
|
+
// has it, and each table's layout while it is still a table authored as it
|
|
6975
|
+
// was when the viewer changed it (a new authored layout, an edit or the
|
|
6976
|
+
// agent's, is shown as authored). On another report it starts again: match
|
|
6977
|
+
// 'all', no drill, every table as authored and nothing temporary.
|
|
6890
6978
|
_prunePageState(report) {
|
|
6891
6979
|
if (this._report()?.id !== report.id) {
|
|
6892
6980
|
this._match.set('all');
|
|
6893
6981
|
this._pageId.set(null);
|
|
6894
6982
|
this._drills.set(new Map());
|
|
6983
|
+
this._gridLayouts.set(new Map());
|
|
6895
6984
|
this._temporary.set(emptyTemporaryState());
|
|
6896
6985
|
}
|
|
6897
6986
|
this._report.set(report);
|
|
6898
6987
|
this._drills.set(new Map([...this._drills()]
|
|
6899
6988
|
.filter(([componentId]) => this._isChart(componentId))));
|
|
6989
|
+
this._pruneGridLayouts();
|
|
6900
6990
|
if (this._pageId() && !(report.pages ?? []).some((page) => page.id === this._pageId())) {
|
|
6901
6991
|
this._pageId.set(null);
|
|
6902
6992
|
}
|
|
@@ -8066,6 +8156,277 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
8066
8156
|
], template: "<div\n class=\"chart-host\"\n echarts\n [options]=\"options() ?? {}\"\n [theme]=\"'report'\"\n [autoResize]=\"true\"\n (chartInit)=\"onChartInit($event)\"\n (chartClick)=\"onChartClick($event)\"\n (chartLegendSelectChanged)=\"onLegendSelectChanged($event)\">\n</div>\n<app-report-component-note [data]=\"data()\"></app-report-component-note>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.chart-host{flex:1 1 0;width:100%;min-height:0}\n"] }]
|
|
8067
8157
|
}] });
|
|
8068
8158
|
|
|
8159
|
+
// What a failed read says when the server gave no message of its own.
|
|
8160
|
+
const READ_FAILED = 'Failed to load data';
|
|
8161
|
+
// A stream that ended before its summary was cut off on the way: the parts
|
|
8162
|
+
// it brought may be short, so the read counts as failed.
|
|
8163
|
+
const STREAM_CUT = 'The table\'s answer was cut off. Try again.';
|
|
8164
|
+
// One grid tile's rows, read through the report: fs-grid asks, and each ask
|
|
8165
|
+
// is one componentGrid request. The params (ReportGridParams) are mapped onto
|
|
8166
|
+
// the request body's own names: the report filters go to `filters`, the 'any'
|
|
8167
|
+
// groups and the grain to `state`.
|
|
8168
|
+
//
|
|
8169
|
+
// The answer streams. Its report line is held in `report`, so output cells
|
|
8170
|
+
// format and the note strip fills from the first paint; each grid line is
|
|
8171
|
+
// passed on as one part, its output fields described as today's header
|
|
8172
|
+
// tooltip describes them (outputColumnNote); the envelope's summary is the
|
|
8173
|
+
// last part. A stream that ends without its summary is a failure.
|
|
8174
|
+
//
|
|
8175
|
+
// Every failure reaches the grid as an Error carrying the server's message:
|
|
8176
|
+
// a streamed request shows no error dialog of its own, so the grid's error
|
|
8177
|
+
// state is where it is read.
|
|
8178
|
+
class ReportGridSource {
|
|
8179
|
+
_reportData;
|
|
8180
|
+
_reportId;
|
|
8181
|
+
_componentId;
|
|
8182
|
+
// The report line of the latest read, null until one arrives.
|
|
8183
|
+
report = signal(null);
|
|
8184
|
+
_isExport;
|
|
8185
|
+
_locale;
|
|
8186
|
+
constructor(_reportData, _reportId, _componentId, options = {}) {
|
|
8187
|
+
this._reportData = _reportData;
|
|
8188
|
+
this._reportId = _reportId;
|
|
8189
|
+
this._componentId = _componentId;
|
|
8190
|
+
this._isExport = options.isExport ?? false;
|
|
8191
|
+
this._locale = options.locale ?? (() => REPORT_FALLBACK_LOCALE);
|
|
8192
|
+
}
|
|
8193
|
+
read(request, params) {
|
|
8194
|
+
return defer(() => {
|
|
8195
|
+
let summary = null;
|
|
8196
|
+
return this._stream$(request, params)
|
|
8197
|
+
.pipe(mergeMap((event) => {
|
|
8198
|
+
if (event.type === StreamEventType.Complete) {
|
|
8199
|
+
summary = envelopeSummary(event) ?? summary;
|
|
8200
|
+
return EMPTY;
|
|
8201
|
+
}
|
|
8202
|
+
return this._part$(event.data);
|
|
8203
|
+
}), concatWith(defer(() => (summary ? of(summary) : throwError(() => new Error(STREAM_CUT))))), catchError$1((error) => throwError(() => readError(error))));
|
|
8204
|
+
});
|
|
8205
|
+
}
|
|
8206
|
+
_stream$(request, params) {
|
|
8207
|
+
return this._reportData.componentGrid(this._reportId, this._componentId, params.filters ?? [], requestState(params), request, this._isExport);
|
|
8208
|
+
}
|
|
8209
|
+
// One data line: the report line is held, a grid line is passed on as a
|
|
8210
|
+
// part, and every other line is skipped.
|
|
8211
|
+
_part$(data) {
|
|
8212
|
+
const line = data;
|
|
8213
|
+
if (line?.report) {
|
|
8214
|
+
this.report.set(line.report);
|
|
8215
|
+
return EMPTY;
|
|
8216
|
+
}
|
|
8217
|
+
return line?.grid ? of(this._described(line.grid)) : EMPTY;
|
|
8218
|
+
}
|
|
8219
|
+
// A part whose fields say what each output holds, as today's header
|
|
8220
|
+
// tooltip says it. A field the report says nothing of keeps its own.
|
|
8221
|
+
_described(part) {
|
|
8222
|
+
if (!part.fields?.length) {
|
|
8223
|
+
return part;
|
|
8224
|
+
}
|
|
8225
|
+
const report = this.report();
|
|
8226
|
+
const locale = this._locale();
|
|
8227
|
+
return {
|
|
8228
|
+
...part,
|
|
8229
|
+
fields: part.fields.map((field) => {
|
|
8230
|
+
const note = outputColumnNote(field.name, report, locale);
|
|
8231
|
+
return note ? { ...field, description: note } : field;
|
|
8232
|
+
}),
|
|
8233
|
+
};
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
// The tile state the data request carries for a grid: the 'any' groups and
|
|
8237
|
+
// the grain, each only when set.
|
|
8238
|
+
function requestState(params) {
|
|
8239
|
+
return {
|
|
8240
|
+
...(params.matchAnyGroupIds?.length ? { matchAnyGroupIds: [...params.matchAnyGroupIds] } : {}),
|
|
8241
|
+
...(params.frequency ? { frequency: params.frequency } : {}),
|
|
8242
|
+
};
|
|
8243
|
+
}
|
|
8244
|
+
// The read's summary from the envelope that closes the stream
|
|
8245
|
+
// ({data: {data: {grid}}, code}), or null for the bare end-of-response event.
|
|
8246
|
+
function envelopeSummary(event) {
|
|
8247
|
+
return event.data?.data?.data?.grid ?? null;
|
|
8248
|
+
}
|
|
8249
|
+
// Any failure as an Error: the server's message when it sent one.
|
|
8250
|
+
function readError(error) {
|
|
8251
|
+
if (error instanceof HttpErrorResponse) {
|
|
8252
|
+
const message = error.error?.message;
|
|
8253
|
+
return new Error(typeof message === 'string' && message ? message : READ_FAILED);
|
|
8254
|
+
}
|
|
8255
|
+
return error instanceof Error ? error : new Error(READ_FAILED);
|
|
8256
|
+
}
|
|
8257
|
+
|
|
8258
|
+
const VALUE_FORMATS = {
|
|
8259
|
+
[GridFormat.Number]: ReportValueFormat.Number,
|
|
8260
|
+
[GridFormat.Percent]: ReportValueFormat.Percent,
|
|
8261
|
+
[GridFormat.Currency]: ReportValueFormat.Currency,
|
|
8262
|
+
};
|
|
8263
|
+
const NUMBER_UNITS = {
|
|
8264
|
+
[GridUnits.None]: ReportNumberUnits.None,
|
|
8265
|
+
[GridUnits.Thousands]: ReportNumberUnits.Thousands,
|
|
8266
|
+
[GridUnits.Millions]: ReportNumberUnits.Millions,
|
|
8267
|
+
[GridUnits.Auto]: ReportNumberUnits.Auto,
|
|
8268
|
+
};
|
|
8269
|
+
// What an abbreviated number's suffix multiplies it by, when a viewer types one.
|
|
8270
|
+
const UNIT_SUFFIXES = { k: 1e3, m: 1e6 };
|
|
8271
|
+
const YES = 'Yes';
|
|
8272
|
+
const NO = 'No';
|
|
8273
|
+
// A grid tile's formatter: every value written as the report writes it on
|
|
8274
|
+
// a list today. An output number reads through formatOutputValue with what
|
|
8275
|
+
// the report line says the output holds (a change is signed, a missing prior
|
|
8276
|
+
// reads as the list's dash); a plain number through formatNumber; a date in
|
|
8277
|
+
// the report's locale, an instant in its zone. The placement's format,
|
|
8278
|
+
// precision and units win over the field's.
|
|
8279
|
+
function reportGridFormatter(context) {
|
|
8280
|
+
return {
|
|
8281
|
+
format: (value, field, placement) => formatGridValue(context, value, field, placement),
|
|
8282
|
+
toNumber: (text, field, placement) => readGridNumber(context, text, field, placement),
|
|
8283
|
+
};
|
|
8284
|
+
}
|
|
8285
|
+
function formatGridValue(context, value, field, placement) {
|
|
8286
|
+
if (value === null || value === undefined || value === '') {
|
|
8287
|
+
return '';
|
|
8288
|
+
}
|
|
8289
|
+
switch (field.type) {
|
|
8290
|
+
case GridFieldType.Number:
|
|
8291
|
+
return formatGridNumber(context, value, field, placement);
|
|
8292
|
+
case GridFieldType.Date:
|
|
8293
|
+
case GridFieldType.Datetime:
|
|
8294
|
+
return formatReportDate(value, context.timezone, context.format.locale);
|
|
8295
|
+
case GridFieldType.Boolean:
|
|
8296
|
+
return value ? YES : NO;
|
|
8297
|
+
default:
|
|
8298
|
+
return optionLabel(value, field) ?? String(value);
|
|
8299
|
+
}
|
|
8300
|
+
}
|
|
8301
|
+
function formatGridNumber(context, value, field, placement) {
|
|
8302
|
+
const spec = numberSpec(field, placement);
|
|
8303
|
+
const output = isAggregate(placement) ? null : context.report?.outputs?.[field.name];
|
|
8304
|
+
return output
|
|
8305
|
+
? formatOutputValue(value, output, spec, context.format).text
|
|
8306
|
+
: formatNumber(value, spec, context.format);
|
|
8307
|
+
}
|
|
8308
|
+
// The format, precision and units a number is written with: the placement's,
|
|
8309
|
+
// else the field's.
|
|
8310
|
+
function numberSpec(field, placement) {
|
|
8311
|
+
const placed = placement;
|
|
8312
|
+
const format = placed?.format ?? field.format;
|
|
8313
|
+
const precision = placed?.precision ?? field.precision;
|
|
8314
|
+
const units = placed?.units ?? field.units;
|
|
8315
|
+
return {
|
|
8316
|
+
...(format ? { format: VALUE_FORMATS[format] } : {}),
|
|
8317
|
+
...(typeof precision === 'number' ? { precision } : {}),
|
|
8318
|
+
...(units ? { units: NUMBER_UNITS[units] } : {}),
|
|
8319
|
+
};
|
|
8320
|
+
}
|
|
8321
|
+
// A value placement is an aggregate of the field, not the output itself.
|
|
8322
|
+
function isAggregate(placement) {
|
|
8323
|
+
return !!placement?.aggregation;
|
|
8324
|
+
}
|
|
8325
|
+
function optionLabel(value, field) {
|
|
8326
|
+
return field.options?.find((option) => option.key === value)?.label ?? null;
|
|
8327
|
+
}
|
|
8328
|
+
// A number a viewer typed, read in the report's locale: its grouping dropped,
|
|
8329
|
+
// its decimal mark read, a K or M suffix multiplied out, and a percent of a
|
|
8330
|
+
// ratio-percent report read back to its ratio. null when it is no number.
|
|
8331
|
+
function readGridNumber(context, text, field, placement) {
|
|
8332
|
+
const decimal = decimalMark(context.format.locale);
|
|
8333
|
+
const trimmed = text.trim().toLowerCase();
|
|
8334
|
+
const multiplier = UNIT_SUFFIXES[trimmed.slice(-1)] ?? 1;
|
|
8335
|
+
const digits = trimmed
|
|
8336
|
+
.split(decimal).map((part) => part.replace(/[^0-9-]/g, '')).join('.');
|
|
8337
|
+
const number = digits && digits !== '-' && digits !== '.' ? Number(digits) * multiplier : NaN;
|
|
8338
|
+
if (!Number.isFinite(number)) {
|
|
8339
|
+
return null;
|
|
8340
|
+
}
|
|
8341
|
+
const isPercent = (placement?.format ?? field.format) === GridFormat.Percent;
|
|
8342
|
+
return isPercent && context.format.ratioPercent ? number / 100 : number;
|
|
8343
|
+
}
|
|
8344
|
+
function decimalMark(locale) {
|
|
8345
|
+
return numberFormat(locale, {}).formatToParts(1.5).find((part) => part.type === 'decimal')?.value ?? '.';
|
|
8346
|
+
}
|
|
8347
|
+
|
|
8348
|
+
// A grid tile: fs-grid over the tile's rows, read through ReportGridSource.
|
|
8349
|
+
//
|
|
8350
|
+
// - The params (the report filters the tile resolves to and the 'any'
|
|
8351
|
+
// groups) are seeded before the first render, so no read goes out without
|
|
8352
|
+
// them. After that a change to one of the tile's filter groups settles for
|
|
8353
|
+
// TILE_REFRESH_DEBOUNCE_MS, so a sweep of changes costs one read; one sent
|
|
8354
|
+
// under an older state version is dropped.
|
|
8355
|
+
// - The layout on screen is the state service's (gridLayout): the viewer's
|
|
8356
|
+
// own while they changed it from the layout authored now, else the
|
|
8357
|
+
// authored one. It outlives a page switch, and the chat reads it.
|
|
8358
|
+
// - A state version move (Clear filters, a saved filter) skips the wait; the
|
|
8359
|
+
// reset has put the authored layout back in the same tick, so it costs one
|
|
8360
|
+
// read.
|
|
8361
|
+
// - fs-grid compares the layout and params by value: a cloned value never
|
|
8362
|
+
// reads again.
|
|
8363
|
+
class ComponentGridComponent {
|
|
8364
|
+
reportId = input.required();
|
|
8365
|
+
component = input.required();
|
|
8366
|
+
params = signal(null);
|
|
8367
|
+
controls = computed(() => this.component().config?.controls ?? {});
|
|
8368
|
+
source = computed(() => new ReportGridSource(this._reportData, this.reportId(), this._componentId(), {
|
|
8369
|
+
locale: () => this._filterState.formatContext().locale,
|
|
8370
|
+
}));
|
|
8371
|
+
layout = computed(() => this._filterState.gridLayout(this.component()));
|
|
8372
|
+
noteData = computed(() => this.source().report());
|
|
8373
|
+
formatter = computed(() => reportGridFormatter({
|
|
8374
|
+
format: this._filterState.formatContext(),
|
|
8375
|
+
timezone: this._filterState.timezone(),
|
|
8376
|
+
report: this.source().report(),
|
|
8377
|
+
}));
|
|
8378
|
+
_componentId = computed(() => this.component().id);
|
|
8379
|
+
_reportData = inject(ReportData);
|
|
8380
|
+
_filterState = inject(ReportFilterStateService);
|
|
8381
|
+
_destroyRef = inject(DestroyRef);
|
|
8382
|
+
// The state version the params on screen were read under.
|
|
8383
|
+
_paramsVersion = 0;
|
|
8384
|
+
ngOnInit() {
|
|
8385
|
+
this._init();
|
|
8386
|
+
}
|
|
8387
|
+
setLayout(layout) {
|
|
8388
|
+
this._filterState.setGridLayout(this.component(), layout);
|
|
8389
|
+
}
|
|
8390
|
+
_init() {
|
|
8391
|
+
this._paramsVersion = this._filterState.stateVersion();
|
|
8392
|
+
this.params.set(this._readParams());
|
|
8393
|
+
this._filterState.changesFor(this.component())
|
|
8394
|
+
.pipe(switchMap$1(() => this._settled$()), tap$1(() => this._applyParams()), takeUntilDestroyed(this._destroyRef))
|
|
8395
|
+
.subscribe();
|
|
8396
|
+
}
|
|
8397
|
+
// A change, once it has settled: at once when the state version moved,
|
|
8398
|
+
// else after the debounce; dropped when the version moves while it waits,
|
|
8399
|
+
// since that move brings its own change.
|
|
8400
|
+
_settled$() {
|
|
8401
|
+
const version = this._filterState.stateVersion();
|
|
8402
|
+
const moved = version !== this._paramsVersion;
|
|
8403
|
+
return (moved ? of(0) : timer(TILE_REFRESH_DEBOUNCE_MS))
|
|
8404
|
+
.pipe(filter$1(() => this._filterState.stateVersion() === version), map$1(() => undefined));
|
|
8405
|
+
}
|
|
8406
|
+
_applyParams() {
|
|
8407
|
+
this._paramsVersion = this._filterState.stateVersion();
|
|
8408
|
+
this.params.set(this._readParams());
|
|
8409
|
+
}
|
|
8410
|
+
// The tile's report filters and 'any' groups as they stand.
|
|
8411
|
+
_readParams() {
|
|
8412
|
+
const component = this.component();
|
|
8413
|
+
const matchAnyGroupIds = this._filterState.tileState(component).matchAnyGroupIds;
|
|
8414
|
+
return {
|
|
8415
|
+
filters: this._filterState.resolveForComponent(component),
|
|
8416
|
+
...(matchAnyGroupIds?.length ? { matchAnyGroupIds } : {}),
|
|
8417
|
+
};
|
|
8418
|
+
}
|
|
8419
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8420
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentGridComponent, isStandalone: true, selector: "app-report-component-grid", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: true, isRequired: true, transformFunction: null }, component: { classPropertyName: "component", publicName: "component", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<!-- The comparison and window note strip above the table, from the report\n line of the latest read; the windows' descriptions and notes go in each\n column's \"About this column\" instead. -->\n<div class=\"fs-column grid-pane\">\n <app-report-component-note\n [data]=\"noteData()\"\n [windowNotes]=\"false\">\n </app-report-component-note>\n <!-- The params are seeded before the first render, so the grid's first read\n carries the tile's report filters. -->\n @if (params(); as params) {\n <fs-grid\n class=\"fs-flex\"\n [source]=\"source()\"\n [layout]=\"layout()\"\n [params]=\"params\"\n [controls]=\"controls()\"\n [formatter]=\"formatter()\"\n (layoutChange)=\"setLayout($event)\">\n </fs-grid>\n }\n</div>\n", styles: [":host{display:block;height:100%}.grid-pane{height:100%;min-height:0}fs-grid{min-height:0}\n"], dependencies: [{ kind: "component", type: FsGridComponent, selector: "fs-grid", inputs: ["source", "layout", "controls", "params", "expansion", "panel", "editorOpen", "formatter", "clearedFilter"], outputs: ["layoutChange", "expansionChange", "panelChange", "editorOpenChange"] }, { kind: "component", type: ComponentNoteComponent, selector: "app-report-component-note", inputs: ["data", "windows", "windowDescriptions", "windowNotes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8421
|
+
}
|
|
8422
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentGridComponent, decorators: [{
|
|
8423
|
+
type: Component,
|
|
8424
|
+
args: [{ selector: 'app-report-component-grid', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
|
|
8425
|
+
FsGridComponent,
|
|
8426
|
+
ComponentNoteComponent,
|
|
8427
|
+
], template: "<!-- The comparison and window note strip above the table, from the report\n line of the latest read; the windows' descriptions and notes go in each\n column's \"About this column\" instead. -->\n<div class=\"fs-column grid-pane\">\n <app-report-component-note\n [data]=\"noteData()\"\n [windowNotes]=\"false\">\n </app-report-component-note>\n <!-- The params are seeded before the first render, so the grid's first read\n carries the tile's report filters. -->\n @if (params(); as params) {\n <fs-grid\n class=\"fs-flex\"\n [source]=\"source()\"\n [layout]=\"layout()\"\n [params]=\"params\"\n [controls]=\"controls()\"\n [formatter]=\"formatter()\"\n (layoutChange)=\"setLayout($event)\">\n </fs-grid>\n }\n</div>\n", styles: [":host{display:block;height:100%}.grid-pane{height:100%;min-height:0}fs-grid{min-height:0}\n"] }]
|
|
8428
|
+
}] });
|
|
8429
|
+
|
|
8069
8430
|
// A KPI's trend over its last periods: a line that stretches to the card's
|
|
8070
8431
|
// width, broken where a period has no value, with the last point marked
|
|
8071
8432
|
// (hollow while its period is still running). The markers are drawn over the
|
|
@@ -9585,7 +9946,7 @@ class ComponentTextComponent {
|
|
|
9585
9946
|
}
|
|
9586
9947
|
}
|
|
9587
9948
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9588
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentTextComponent, isStandalone: true, selector: "app-report-component-text", inputs: { html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: true, transformFunction: null }, editing: { classPropertyName: "editing", publicName: "editing", isSignal: true, isRequired: false, transformFunction: null }, shownElsewhere: { classPropertyName: "shownElsewhere", publicName: "shownElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { saved: "saved", editEnded: "editEnded", valueSettings: "valueSettings" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "style.--text-size": "typeScale.body", "style.--text-h1-size": "typeScale.h1", "style.--text-h2-size": "typeScale.h2", "style.--text-h3-size": "typeScale.h3", "style.--text-color": "typeScale.color", "style.--text-table-border": "tableLook.border", "style.--text-table-padding": "tableLook.padding", "style.--text-table-margin": "tableLook.margin" } }, viewQueries: [{ propertyName: "_editor", first: true, predicate: FsHtmlEditorComponent, descendants: true, isSignal: true }, { propertyName: "_valuePopoverAnchor", first: true, predicate: ["valuePopoverAnchor"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:hidden;font-family:Arial,Helvetica,sans-serif;font-size:var(--text-size);line-height:1.25;color:var(--text-color);word-wrap:break-word;letter-spacing:normal;word-spacing:normal;font-weight:400;font-style:normal;text-transform:none;white-space:normal}.text-content ::ng-deep p,.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3,.text-content ::ng-deep ul,.text-content ::ng-deep ol{margin:0}.text-content ::ng-deep h1{font-size:var(--text-h1-size)}.text-content ::ng-deep h2{font-size:var(--text-h2-size)}.text-content ::ng-deep h3{font-size:var(--text-h3-size)}.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3{font-weight:700;line-height:1.2}.text-content ::ng-deep ul,.text-content ::ng-deep ol{padding-left:1.4em}.text-content ::ng-deep p:empty:before{content:\"\\200b\"}.text-content ::ng-deep a{color:inherit}.text-content ::ng-deep table{border-collapse:collapse;margin:var(--text-table-margin) 0}.text-content ::ng-deep th,.text-content ::ng-deep td{border:var(--text-table-border);padding:var(--text-table-padding);vertical-align:middle;text-align:left}.text-content ::ng-deep th{text-align:center;font-weight:700}.text-content.editing{overflow:auto;cursor:text}.text-content.editing ::ng-deep [data-embed-key],.text-content.editing ::ng-deep [data-embed-field]{padding:0 2px;border-radius:3px;background-color:#4e79a729;-webkit-user-select:all;user-select:all}.text-content.editing ::ng-deep [data-embed-key]{cursor:pointer}.text-content.editing ::ng-deep [data-embed-key]:hover{background-color:#4e79a747}.text-content.editing ::ng-deep [data-embed-missing]{background-color:#e1575924;outline:1px dashed rgba(225,87,89,.7)}.text-content.editing ::ng-deep th,.text-content.editing ::ng-deep tfoot td{background-color:transparent}.text-content.editing ::ng-deep .fr-table-selector{display:none}.text-content.editing ::ng-deep .fr-table-selection-hover{outline:none}.value-popover-anchor{position:absolute;pointer-events:none}::ng-deep .value-popover-wrapper .popover-content{padding:15px!important}.value-popover{gap:15px}.value-popover-name{font-weight:600}.value-popover-settings{--mdc-icon-button-state-layer-size: 24px;width:24px;height:24px;padding:0}.value-notice{position:absolute;top:100%;left:0;z-index:2;margin-top:4px;padding:2px 4px 2px 10px;border:1px solid #d9dee3;border-radius:4px;background:#fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: FsHtmlEditorComponent, selector: "fs-html-editor", inputs: ["config", "disabled"] }, { kind: "directive", type: FsPopoverDirective, selector: "[fsPopover]", inputs: ["text", "template", "data", "leaveDelay", "showDelay", "maxWidth", "maxHeight", "wrapperClass", "autoShow", "autoClose", "loadingDiameter", "loading", "indication", "position", "theme", "size", "trigger", "enabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9949
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ComponentTextComponent, isStandalone: true, selector: "app-report-component-text", inputs: { html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: true, transformFunction: null }, editing: { classPropertyName: "editing", publicName: "editing", isSignal: true, isRequired: false, transformFunction: null }, shownElsewhere: { classPropertyName: "shownElsewhere", publicName: "shownElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { saved: "saved", editEnded: "editEnded", valueSettings: "valueSettings" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "style.--text-size": "typeScale.body", "style.--text-h1-size": "typeScale.h1", "style.--text-h2-size": "typeScale.h2", "style.--text-h3-size": "typeScale.h3", "style.--text-color": "typeScale.color", "style.--text-table-border": "tableLook.border", "style.--text-table-padding": "tableLook.padding", "style.--text-table-margin": "tableLook.margin" } }, viewQueries: [{ propertyName: "_editor", first: true, predicate: FsHtmlEditorComponent, descendants: true, isSignal: true }, { propertyName: "_valuePopoverAnchor", first: true, predicate: ["valuePopoverAnchor"], descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: "@if (editing()) {\n <!-- Floating fs-html-editor: no frame, Froala's inline toolbar pops up over\n the text. pointerdown stays here so a press in the text never starts a\n drag of the block or a pan of the canvas. -->\n <fs-html-editor\n class=\"text-content editing\"\n [config]=\"editorConfig\"\n [(ngModel)]=\"draft\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n </fs-html-editor>\n\n <!-- One notice at a time: an edit stopped because it would have removed\n values, an edit that left values no text reads, or a refused command.\n mousedown keeps the editor's focus and selection, so Undo reaches it. -->\n @if (notice(); as current) {\n <div\n class=\"value-notice fs-row.gap-sm\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.preventDefault()\">\n @switch (current.kind) {\n @case ('blocked') {\n @if (current.block.removeSelection) {\n <span>\n This selection includes {{ current.block.count }} {{ current.block.count === 1 ? 'value' : 'values' }}.\n </span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"removeBlockedSelection()\">\n Remove text and {{ current.block.count === 1 ? 'value' : current.block.count + ' values' }}\n </button>\n } @else {\n <span>To take a value out, select it, then press Delete.</span>\n }\n }\n @case ('unused') {\n <span>{{ current.count }} {{ current.count === 1 ? 'value' : 'values' }} moved to Not used</span>\n <button\n mat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"undoValuesUnused()\">\n Undo\n </button>\n }\n @case ('message') {\n <span>{{ current.text }}</span>\n }\n }\n <button\n mat-button\n type=\"button\"\n (click)=\"dismissNotice()\">\n Dismiss\n </button>\n </div>\n }\n} @else {\n <div\n class=\"text-content\"\n [innerHTML]=\"displayHtml()\">\n </div>\n}\n\n<!-- A clicked value chip's popover: an fs-popover on an invisible anchor laid\n over the chip (the chip itself belongs to the editor). Outside the editing\n block, so a popover still open when editing ends closes the usual way. -->\n<span\n #valuePopoverAnchor\n class=\"value-popover-anchor\"\n aria-hidden=\"true\"\n fsPopover\n trigger=\"click\"\n wrapperClass=\"value-popover-wrapper\"\n [template]=\"valuePopoverTemplate\"\n [indication]=\"false\"\n [maxWidth]=\"320\"\n [style.left.px]=\"valuePopover()?.left\"\n [style.top.px]=\"valuePopover()?.top\"\n [style.width.px]=\"valuePopover()?.width\"\n [style.height.px]=\"valuePopover()?.height\">\n</span>\n<ng-template\n #valuePopoverTemplate\n let-popover=\"popover\">\n <!-- What the value is, and its settings (where it can also be deleted); a\n chip naming no value on the page has no settings. -->\n <div class=\"value-popover fs-row\">\n <div class=\"fs-flex\">\n <div class=\"value-popover-name\">{{ valuePopoverValue()?.name ?? valuePopover()?.key }}</div>\n @if (!valuePopoverValue()) {\n <div class=\"small\">Not a value on this page</div>\n }\n </div>\n @if (valuePopoverValue()) {\n <button\n mat-icon-button\n type=\"button\"\n class=\"value-popover-settings\"\n matTooltip=\"Settings\"\n (click)=\"openValueSettings(popover)\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;position:relative;width:100%;height:100%}.text-content{display:block;width:100%;height:100%;overflow:hidden;font-family:Arial,Helvetica,sans-serif;font-size:var(--text-size);line-height:1.25;color:var(--text-color);word-wrap:break-word;letter-spacing:normal;word-spacing:normal;font-weight:400;font-style:normal;text-transform:none;white-space:normal}.text-content ::ng-deep p,.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3,.text-content ::ng-deep ul,.text-content ::ng-deep ol{margin:0}.text-content ::ng-deep h1{font-size:var(--text-h1-size)}.text-content ::ng-deep h2{font-size:var(--text-h2-size)}.text-content ::ng-deep h3{font-size:var(--text-h3-size)}.text-content ::ng-deep h1,.text-content ::ng-deep h2,.text-content ::ng-deep h3{font-weight:700;line-height:1.2}.text-content ::ng-deep ul,.text-content ::ng-deep ol{padding-left:1.4em}.text-content ::ng-deep p:empty:before{content:\"\\200b\"}.text-content ::ng-deep a{color:inherit}.text-content ::ng-deep table{border-collapse:collapse;margin:var(--text-table-margin) 0}.text-content ::ng-deep th,.text-content ::ng-deep td{border:var(--text-table-border);padding:var(--text-table-padding);vertical-align:middle;text-align:left}.text-content ::ng-deep th{text-align:center;font-weight:700}.text-content.editing{overflow:auto;cursor:text}.text-content.editing ::ng-deep [data-embed-key],.text-content.editing ::ng-deep [data-embed-field]{padding:0 2px;border-radius:3px;background-color:#4e79a729;-webkit-user-select:all;user-select:all}.text-content.editing ::ng-deep [data-embed-key]{cursor:pointer}.text-content.editing ::ng-deep [data-embed-key]:hover{background-color:#4e79a747}.text-content.editing ::ng-deep [data-embed-missing]{background-color:#e1575924;outline:1px dashed rgba(225,87,89,.7)}.text-content.editing ::ng-deep th,.text-content.editing ::ng-deep tfoot td{background-color:transparent}.text-content.editing ::ng-deep .fr-table-selector{display:none}.text-content.editing ::ng-deep .fr-table-selection-hover{outline:none}.value-popover-anchor{position:absolute;pointer-events:none}::ng-deep .value-popover-wrapper .popover-content{padding:15px!important}.value-popover{gap:15px}.value-popover-name{font-weight:600}.value-popover-settings{--mdc-icon-button-state-layer-size: 24px;width:24px;height:24px;padding:0}.value-notice{position:absolute;top:100%;left:0;z-index:2;margin-top:4px;padding:2px 4px 2px 10px;border:1px solid #d9dee3;border-radius:4px;background:#fff;white-space:nowrap;font-family:Arial,Helvetica,sans-serif}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: FsHtmlEditorComponent, selector: "fs-html-editor", inputs: ["config", "disabled"] }, { kind: "directive", type: FsPopoverDirective, selector: "[fsPopover]", inputs: ["text", "template", "data", "leaveDelay", "showDelay", "maxWidth", "maxHeight", "wrapperClass", "autoShow", "autoClose", "loadingDiameter", "loading", "indication", "position", "theme", "size", "trigger", "enabled", "panel"], outputs: ["closed"], exportAs: ["fsPopover"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9589
9950
|
}
|
|
9590
9951
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentTextComponent, decorators: [{
|
|
9591
9952
|
type: Component,
|
|
@@ -9617,6 +9978,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
9617
9978
|
const HANDLES = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
|
|
9618
9979
|
const MIN_W = 0.6; // inches
|
|
9619
9980
|
const MIN_H = 0.4; // inches
|
|
9981
|
+
// The tiles that read their own rows: a list through FsList paging, a grid
|
|
9982
|
+
// through fs-grid's source. This component loads every other tile's data.
|
|
9983
|
+
const SELF_READING_TYPES = ['list', 'grid'];
|
|
9620
9984
|
// Two presses on a text block this close together open its editor. Detected
|
|
9621
9985
|
// here rather than with a dblclick listener because the drag loop claims the
|
|
9622
9986
|
// pointer on every press, and a browser is not obliged to deliver dblclick
|
|
@@ -9629,7 +9993,8 @@ const DOUBLE_PRESS_MS = 400;
|
|
|
9629
9993
|
// snapper); everything works at any zoom (pixel deltas ÷ zoom·96 → inches).
|
|
9630
9994
|
//
|
|
9631
9995
|
// Charts and KPIs fetch their own data here; lists fetch inside
|
|
9632
|
-
// ComponentListComponent through FsList paging
|
|
9996
|
+
// ComponentListComponent through FsList paging, and grids inside
|
|
9997
|
+
// ComponentGridComponent through fs-grid's source.
|
|
9633
9998
|
class ReportComponentComponent {
|
|
9634
9999
|
reportId;
|
|
9635
10000
|
component;
|
|
@@ -9686,8 +10051,9 @@ class ReportComponentComponent {
|
|
|
9686
10051
|
// repaints the header and body.
|
|
9687
10052
|
filterConfig = signal(null);
|
|
9688
10053
|
data = signal(null);
|
|
9689
|
-
//
|
|
9690
|
-
//
|
|
10054
|
+
// A load shows a spinner centred on the tile. `loading` is the first load,
|
|
10055
|
+
// with nothing to show yet; a refetch keeps the data on screen, dimmed
|
|
10056
|
+
// under the spinner (`refreshing`). The title is never dimmed.
|
|
9691
10057
|
loading = signal(false);
|
|
9692
10058
|
refreshing = signal(false);
|
|
9693
10059
|
error = signal(null);
|
|
@@ -9756,9 +10122,10 @@ class ReportComponentComponent {
|
|
|
9756
10122
|
if (!this.isText) {
|
|
9757
10123
|
this._rebuildHeaderOnNewState();
|
|
9758
10124
|
}
|
|
9759
|
-
// Lists fetch through FsList
|
|
9760
|
-
//
|
|
9761
|
-
|
|
10125
|
+
// Lists fetch through FsList and grids through fs-grid's source (each
|
|
10126
|
+
// reads its own rows); text has nothing to fetch, and a value is never
|
|
10127
|
+
// drawn (it reads inside text).
|
|
10128
|
+
if (!SELF_READING_TYPES.includes(this.component.type) && !this.isText && isDrawn(this.component)) {
|
|
9762
10129
|
// One load stream (tileLoads): the first load at once, then one per
|
|
9763
10130
|
// settled sweep of changes, each cancelling the one in flight, and an
|
|
9764
10131
|
// answer sent before a reset is dropped.
|
|
@@ -10266,8 +10633,7 @@ class ReportComponentComponent {
|
|
|
10266
10633
|
}
|
|
10267
10634
|
// ----- data ----------------------------------------------------------------
|
|
10268
10635
|
// One request of the load stream (tileLoads), built from the state now. The
|
|
10269
|
-
//
|
|
10270
|
-
// data stays and the thin bar shows.
|
|
10636
|
+
// spinner shows either way; a tile with data on screen keeps it, dimmed.
|
|
10271
10637
|
_load() {
|
|
10272
10638
|
const first = !this.data();
|
|
10273
10639
|
this.loading.set(first);
|
|
@@ -10282,7 +10648,7 @@ class ReportComponentComponent {
|
|
|
10282
10648
|
return this._reportData.componentData(this.reportId, this.component.id, filters, state);
|
|
10283
10649
|
}
|
|
10284
10650
|
// A failed load clears the data with its message, so the next load starts
|
|
10285
|
-
// from
|
|
10651
|
+
// from an empty tile and never shows the data the failure replaced. A
|
|
10286
10652
|
// chart's answer gives its drill path.
|
|
10287
10653
|
_loaded(result) {
|
|
10288
10654
|
this.data.set(result.data);
|
|
@@ -10335,22 +10701,23 @@ class ReportComponentComponent {
|
|
|
10335
10701
|
openAtPoint(anchor, click.pointer, this.zoom, () => this.markMenu?.openMenu(null));
|
|
10336
10702
|
}
|
|
10337
10703
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10338
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportComponentComponent, isStandalone: true, selector: "app-report-component", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: false, isRequired: false, transformFunction: null }, component: { classPropertyName: "component", publicName: "component", isSignal: false, isRequired: false, transformFunction: null }, groups: { classPropertyName: "groups", publicName: "groups", isSignal: false, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: false, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: false, isRequired: false, transformFunction: null }, snapper: { classPropertyName: "snapper", publicName: "snapper", isSignal: false, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: false, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: false, transformFunction: null }, valueKeysElsewhere: { classPropertyName: "valueKeysElsewhere", publicName: "valueKeysElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectComponent: "selectComponent", positionChanged: "positionChanged", openSettings: "openSettings", removed: "removed", valueSettings: "valueSettings", textSaved: "textSaved", valuesRemoved: "valuesRemoved" }, host: { properties: { "class.height-auto": "this.heightAuto" } }, viewQueries: [{ propertyName: "_text", first: true, predicate: ComponentTextComponent, descendants: true, isSignal: true }, { propertyName: "chartComponent", first: true, predicate: ComponentChartComponent, descendants: true }, { propertyName: "markMenu", first: true, predicate: ["markMenu"], descendants: true }, { propertyName: "markAnchor", first: true, predicate: ["markAnchor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"component-box\"\n [class.editing]=\"editMode\"\n [class.text]=\"isText\"\n [class.selected]=\"selected\"\n [style.padding]=\"bodyPadding\"\n (pointerdown)=\"onComponentPointerDown($event)\"\n (mousedown)=\"stopCanvasPan($event)\"\n (touchstart)=\"stopCanvasPan($event)\">\n <!-- The header is an fs-filter: heading template = title (and the edit-mode\n drag handle), actions = the menu, items = component-level filter groups,\n the same strip for every type (lists included). fs-filter reads its\n config once, so the block is keyed on the component-filter set to be\n recreated when it changes. The title shows its tokens filled, and a\n filter change repaints it in place (shownTitle). -->\n <!-- A text block has no title bar \u2014 it IS its words, like a text box on a\n slide \u2014 so it gets neither the header nor the data actions. -->\n @for (key of isText ? [] : [filterKey]; track key) {\n <fs-filter\n class=\"component-filter\"\n [config]=\"filterConfig()\">\n <ng-template fsFilterHeading>\n <div\n class=\"component-title\"\n (pointerdown)=\"onDragStart($event)\">\n <!-- While the chart is drilled the title leads its breadcrumb:\n 'Title \u203A 2026 \u203A Q3', the title as the viewer reads it\n (shownTitle). The title goes back to the top and each\n crumb to the period it names; the last is where the chart is.\n A long path is cut at its left end, so the crumbs nearest the\n chart stay in view. While the layout is edited the crumbs are\n plain text (the stylesheet; drillTo does nothing then). -->\n @if (drillCrumbs().length) {\n <span class=\"drill-path\">\n <span dir=\"ltr\">\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo(0)\"\n (keydown.enter)=\"drillTo(0)\">\n {{ shownTitle }}\n </a>\n @for (label of drillCrumbs(); track $index; let last = $last) {\n <span class=\"drill-separator\">{{ drillSeparator }}</span>\n @if (last) {\n <span>{{ label }}</span>\n } @else {\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo($index + 1)\"\n (keydown.enter)=\"drillTo($index + 1)\">\n {{ label }}\n </a>\n }\n }\n </span>\n </span>\n } @else {\n {{ shownTitle }}\n }\n @if (truncated) {\n <mat-icon\n class=\"truncated-icon\"\n matTooltip=\"Showing a truncated result \u2014 refine the filters to see everything.\">\n warning_amber\n </mat-icon>\n }\n </div>\n </ng-template>\n </fs-filter>\n }\n <!-- The actions menu is floated top-right (absolute) rather than living in\n fs-filter's actions slot, so its ~40px button height no longer drives the\n header row's height. It overlays the box corner; the heading reserves\n right padding so a long title ellipsises before it. -->\n @if (!isText || (editMode && !textEditing())) {\n <button\n mat-icon-button\n class=\"component-menu\"\n [fsMenuTriggerFor]=\"menu\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"stopCanvasPan($event)\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <!-- Beside its trigger, inside the same block: an fs-menu that finds no\n trigger of its own draws one. -->\n <fs-menu #menu>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"editText()\">\n <mat-icon>edit</mat-icon>\n <span>Edit text</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"deleteText()\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillUp()\">\n <mat-icon>arrow_upward</mat-icon>\n <span>Drill up</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillTo(0)\">\n <mat-icon>vertical_align_top</mat-icon>\n <span>Back to top</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"settings()\">\n <mat-icon>tune</mat-icon>\n <span>Settings</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportCsv()\">\n <mat-icon>download</mat-icon>\n <span>Export CSV</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!hidesColumns\"\n (click)=\"chooseColumns()\">\n <mat-icon>view_column</mat-icon>\n <span>Columns</span>\n </ng-template>\n </fs-menu>\n }\n <div class=\"component-body\">\n <!-- A refetch keeps the data on screen under a thin bar; the shimmer\n below is for the first load only. -->\n @if (refreshing()) {\n <mat-progress-bar\n class=\"refresh-bar\"\n aria-label=\"Refreshing data\"\n [mode]=\"'indeterminate'\">\n </mat-progress-bar>\n }\n @if (isText) {\n <app-report-component-text\n [html]=\"textHtml() ?? component.config?.html ?? ''\"\n [tokens]=\"tokens()\"\n [editing]=\"textEditing()\"\n [shownElsewhere]=\"shownElsewhere\"\n (saved)=\"onTextSaved($event)\"\n (editEnded)=\"textEditing.set(false)\"\n (valueSettings)=\"valueSettings.emit($event)\">\n </app-report-component-text>\n } @else if (component.type === 'list') {\n @for (key of [listResetKey()]; track key) {\n <app-report-component-list\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-list>\n }\n } @else if (loading()) {\n <div class=\"component-state\">\n <div class=\"loading-shimmer\"></div>\n </div>\n } @else if (error()) {\n <div class=\"component-state error\">\n {{ error() }}\n </div>\n } @else if (!data()?.rows?.length) {\n <div class=\"component-state\">\n {{ emptyMessage() }}\n </div>\n } @else if (component.type === 'kpi') {\n <app-report-component-kpi\n [component]=\"component\"\n [data]=\"data()\"\n (markClick)=\"onMarkClick($event)\">\n </app-report-component-kpi>\n } @else {\n <app-report-component-chart\n [component]=\"component\"\n [data]=\"data()\"\n [actions]=\"tileActions()\"\n [(legendSelected)]=\"legendSelected\"\n (markClick)=\"onMarkClick($event)\"\n (markMiss)=\"onMarkMiss()\">\n </app-report-component-chart>\n }\n <!-- In edit mode a transparent veil over the body makes the WHOLE\n component draggable (industry standard: you grab the object, not just\n its title bar) and keeps inner widgets from swallowing the gesture. -->\n @if (editMode && !textEditing()) {\n <div\n class=\"drag-veil\"\n (pointerdown)=\"onDragStart($event)\">\n </div>\n }\n </div>\n</div>\n<!-- The mark menu, for a click whose mark offers two or more actions. It\n opens at the click: its trigger is a zero-size anchor that\n anchor-at-point.ts moves there first. -->\n@if (!isText && !editMode) {\n <span\n #markAnchor\n [style.position]=\"'absolute'\"\n [fsMenuTriggerFor]=\"markMenu\">\n </span>\n <fs-menu #markMenu>\n @for (action of markMenuActions(); track action) {\n <ng-template\n fs-menu-item\n (click)=\"runMarkAction(action)\">\n {{ markActionLabels[action] }}\n </ng-template>\n }\n </fs-menu>\n}\n<!-- Handles live OUTSIDE the clipped box so they straddle its edges. -->\n@if (editMode && selected) {\n @for (handle of resizeHandles; track handle) {\n <div\n class=\"handle handle-{{ handle }}\"\n (mousedown)=\"stopCanvasPan($event)\"\n (pointerdown)=\"onResizeStart($event, handle)\">\n </div>\n }\n}", styles: [":host{position:absolute;display:flex;flex-direction:column;font-size:10px;--mdc-outlined-button-container-height: 30px;--mdc-outlined-button-label-text-size: 10px;--mat-outlined-button-horizontal-padding: 15px;--mdc-text-button-container-height: 30px;--mdc-text-button-label-text-size: 10px;--mat-text-button-horizontal-padding: 15px;--mat-button-outlined-container-height: 30px;--mat-button-outlined-label-text-size: 10px;--mat-button-outlined-horizontal-padding: 15px;--mat-button-text-container-height: 30px;--mat-button-text-label-text-size: 10px;--mat-button-text-horizontal-padding: 15px}.component-box{position:relative;width:100%;flex:1 1 auto;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;background:#fff;border:1px solid #e4e7eb;border-radius:6px;overflow:hidden;transition:box-shadow .12s ease,border-color .12s ease;cursor:default}.component-box.editing .component-title{cursor:grab}.component-box.editing .component-title:active{cursor:grabbing}.component-box.editing .drill-crumb{pointer-events:none}.component-box.editing:hover{border-color:#9db3c8;box-shadow:0 2px 8px #0f172a14}.component-box.selected{border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color),0 4px 14px #2196f32e}.component-box.text{background:transparent;border-color:transparent;border-radius:0;overflow:visible}.component-box.text.editing{border-style:dashed;border-color:#cbd2d9}.component-box.text.editing:hover{box-shadow:none}.component-box.text.selected{border-style:solid;border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color)}:host ::ng-deep .component-filter .filter-bar-container{align-items:center!important}.component-filter{flex:0 0 auto;display:block;padding:0 0 4px}.component-filter .component-title{display:flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;padding-right:40px;font-size:var(--report-heading-size);font-weight:600;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.component-filter .component-title .truncated-icon{font-size:16px;width:16px;height:16px;color:#f59e0b}.component-filter .component-title .drill-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.component-filter .component-title .drill-crumb{color:inherit;cursor:pointer;text-decoration:none}.component-filter .component-title .drill-crumb:hover,.component-filter .component-title .drill-crumb:focus-visible{text-decoration:underline}.component-filter .component-title .drill-separator{opacity:.6}.component-menu{position:absolute;top:2px;right:2px;z-index:30}.component-body{flex:1 1 auto;min-height:0;position:relative}.refresh-bar{position:absolute;top:0;left:0;right:0;z-index:1;pointer-events:none;--mdc-linear-progress-track-height: 2px;--mdc-linear-progress-active-indicator-height: 2px;--mat-progress-bar-track-height: 2px;--mat-progress-bar-active-indicator-height: 2px}:host(.height-auto) .component-state{min-height:120px}.drag-veil{position:absolute;inset:0;cursor:grab;touch-action:none}.drag-veil:active{cursor:grabbing}.component-state{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:12px;color:#7b8794;padding:8px;text-align:center}.component-state.error{color:#e15759}.loading-shimmer{width:70%;height:60%;border-radius:6px;background:linear-gradient(100deg,#f0f4f8 40%,#e4ebf2,#f0f4f8 60%);background-size:200% 100%;animation:shimmer 1.2s infinite linear}@keyframes shimmer{to{background-position-x:-200%}}.handle{position:absolute;width:14px;height:14px;display:flex;align-items:center;justify-content:center;touch-action:none;z-index:20;transform:scale(calc(1 / var(--canvas-zoom, 1)))}.handle:before{content:\"\";width:8px;height:8px;background:#fff;border:1.5px solid var(--brand-primary-color);border-radius:2px;box-shadow:0 1px 2px #0f172a33}.handle-nw{left:-7px;top:-7px;cursor:nwse-resize}.handle-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}.handle-ne{right:-7px;top:-7px;cursor:nesw-resize}.handle-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}.handle-se{right:-7px;bottom:-7px;cursor:nwse-resize}.handle-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}.handle-sw{left:-7px;bottom:-7px;cursor:nesw-resize}.handle-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}\n"], dependencies: [{ kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "directive", type: i1$2.FilterHeadingDirective, selector: "[fsFilterHeading]" }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: ComponentChartComponent, selector: "app-report-component-chart", inputs: ["component", "data", "legendSelected", "actions"], outputs: ["legendSelectedChange", "markClick", "markMiss"] }, { kind: "component", type: ComponentKpiComponent, selector: "app-report-component-kpi", inputs: ["component", "data"], outputs: ["markClick"] }, { kind: "component", type: ComponentListComponent, selector: "app-report-component-list", inputs: ["reportId", "component"] }, { kind: "component", type: ComponentTextComponent, selector: "app-report-component-text", inputs: ["html", "tokens", "editing", "shownElsewhere"], outputs: ["saved", "editEnded", "valueSettings"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10704
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportComponentComponent, isStandalone: true, selector: "app-report-component", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: false, isRequired: false, transformFunction: null }, component: { classPropertyName: "component", publicName: "component", isSignal: false, isRequired: false, transformFunction: null }, groups: { classPropertyName: "groups", publicName: "groups", isSignal: false, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: false, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: false, isRequired: false, transformFunction: null }, snapper: { classPropertyName: "snapper", publicName: "snapper", isSignal: false, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: false, isRequired: false, transformFunction: null }, tokens: { classPropertyName: "tokens", publicName: "tokens", isSignal: true, isRequired: false, transformFunction: null }, valueKeysElsewhere: { classPropertyName: "valueKeysElsewhere", publicName: "valueKeysElsewhere", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectComponent: "selectComponent", positionChanged: "positionChanged", openSettings: "openSettings", removed: "removed", valueSettings: "valueSettings", textSaved: "textSaved", valuesRemoved: "valuesRemoved" }, host: { properties: { "class.height-auto": "this.heightAuto" } }, viewQueries: [{ propertyName: "_text", first: true, predicate: ComponentTextComponent, descendants: true, isSignal: true }, { propertyName: "chartComponent", first: true, predicate: ComponentChartComponent, descendants: true }, { propertyName: "markMenu", first: true, predicate: ["markMenu"], descendants: true }, { propertyName: "markAnchor", first: true, predicate: ["markAnchor"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"component-box\"\n [class.editing]=\"editMode\"\n [class.text]=\"isText\"\n [class.selected]=\"selected\"\n [class.refreshing]=\"refreshing()\"\n [style.padding]=\"bodyPadding\"\n (pointerdown)=\"onComponentPointerDown($event)\"\n (mousedown)=\"stopCanvasPan($event)\"\n (touchstart)=\"stopCanvasPan($event)\">\n <!-- The header is an fs-filter: heading template = title (and the edit-mode\n drag handle), actions = the menu, items = component-level filter groups,\n the same strip for every type (lists included). fs-filter reads its\n config once, so the block is keyed on the component-filter set to be\n recreated when it changes. The title shows its tokens filled, and a\n filter change repaints it in place (shownTitle). -->\n <!-- A text block has no title bar \u2014 it IS its words, like a text box on a\n slide \u2014 so it gets neither the header nor the data actions. -->\n @for (key of isText ? [] : [filterKey]; track key) {\n <fs-filter\n class=\"component-filter\"\n [config]=\"filterConfig()\">\n <ng-template fsFilterHeading>\n <div\n class=\"component-title\"\n (pointerdown)=\"onDragStart($event)\">\n <!-- While the chart is drilled the title leads its breadcrumb:\n 'Title \u203A 2026 \u203A Q3', the title as the viewer reads it\n (shownTitle). The title goes back to the top and each\n crumb to the period it names; the last is where the chart is.\n A long path is cut at its left end, so the crumbs nearest the\n chart stay in view. While the layout is edited the crumbs are\n plain text (the stylesheet; drillTo does nothing then). -->\n @if (drillCrumbs().length) {\n <span class=\"drill-path\">\n <span dir=\"ltr\">\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo(0)\"\n (keydown.enter)=\"drillTo(0)\">\n {{ shownTitle }}\n </a>\n @for (label of drillCrumbs(); track $index; let last = $last) {\n <span class=\"drill-separator\">{{ drillSeparator }}</span>\n @if (last) {\n <span>{{ label }}</span>\n } @else {\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo($index + 1)\"\n (keydown.enter)=\"drillTo($index + 1)\">\n {{ label }}\n </a>\n }\n }\n </span>\n </span>\n } @else {\n {{ shownTitle }}\n }\n @if (truncated) {\n <mat-icon\n class=\"truncated-icon\"\n matTooltip=\"Showing a truncated result \u2014 refine the filters to see everything.\">\n warning_amber\n </mat-icon>\n }\n </div>\n </ng-template>\n </fs-filter>\n }\n <!-- The actions menu is floated top-right (absolute) rather than living in\n fs-filter's actions slot, so its ~40px button height no longer drives the\n header row's height. It overlays the box corner; the heading reserves\n right padding so a long title ellipsises before it. -->\n @if (!isText || (editMode && !textEditing())) {\n <button\n mat-icon-button\n class=\"component-menu\"\n [fsMenuTriggerFor]=\"menu\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"stopCanvasPan($event)\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <!-- Beside its trigger, inside the same block: an fs-menu that finds no\n trigger of its own draws one. -->\n <fs-menu #menu>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"editText()\">\n <mat-icon>edit</mat-icon>\n <span>Edit text</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"deleteText()\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillUp()\">\n <mat-icon>arrow_upward</mat-icon>\n <span>Drill up</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillTo(0)\">\n <mat-icon>vertical_align_top</mat-icon>\n <span>Back to top</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"settings()\">\n <mat-icon>tune</mat-icon>\n <span>Settings</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportCsv()\">\n <mat-icon>download</mat-icon>\n <span>Export CSV</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!hidesColumns\"\n (click)=\"chooseColumns()\">\n <mat-icon>view_column</mat-icon>\n <span>Columns</span>\n </ng-template>\n </fs-menu>\n }\n <div class=\"component-body\">\n @if (isText) {\n <app-report-component-text\n [html]=\"textHtml() ?? component.config?.html ?? ''\"\n [tokens]=\"tokens()\"\n [editing]=\"textEditing()\"\n [shownElsewhere]=\"shownElsewhere\"\n (saved)=\"onTextSaved($event)\"\n (editEnded)=\"textEditing.set(false)\"\n (valueSettings)=\"valueSettings.emit($event)\">\n </app-report-component-text>\n } @else if (component.type === 'list') {\n @for (key of [listResetKey()]; track key) {\n <app-report-component-list\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-list>\n }\n } @else if (component.type === 'grid') {\n <app-report-component-grid\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-grid>\n } @else if (loading()) {\n <!-- Holds the tile's height for the spinner on the first load. -->\n <div class=\"component-state\"></div>\n } @else if (error()) {\n <div class=\"component-state error\">\n {{ error() }}\n </div>\n } @else if (!data()?.rows?.length) {\n <div class=\"component-state\">\n {{ emptyMessage() }}\n </div>\n } @else if (component.type === 'kpi') {\n <app-report-component-kpi\n [component]=\"component\"\n [data]=\"data()\"\n (markClick)=\"onMarkClick($event)\">\n </app-report-component-kpi>\n } @else {\n <app-report-component-chart\n [component]=\"component\"\n [data]=\"data()\"\n [actions]=\"tileActions()\"\n [(legendSelected)]=\"legendSelected\"\n (markClick)=\"onMarkClick($event)\"\n (markMiss)=\"onMarkMiss()\">\n </app-report-component-chart>\n }\n <!-- In edit mode a transparent veil over the body makes the WHOLE\n component draggable (industry standard: you grab the object, not just\n its title bar) and keeps inner widgets from swallowing the gesture. -->\n @if (editMode && !textEditing()) {\n <div\n class=\"drag-veil\"\n (pointerdown)=\"onDragStart($event)\">\n </div>\n }\n </div>\n <!-- A load: a spinner centred on the whole tile. On a refetch the body and\n the menu are dimmed under it (.refreshing); the title is not. -->\n @if (loading() || refreshing()) {\n <mat-progress-spinner\n class=\"load-spinner\"\n aria-label=\"Loading data\"\n mode=\"indeterminate\"\n [diameter]=\"15\">\n </mat-progress-spinner>\n }\n</div>\n<!-- The mark menu, for a click whose mark offers two or more actions. It\n opens at the click: its trigger is a zero-size anchor that\n anchor-at-point.ts moves there first. -->\n@if (!isText && !editMode) {\n <span\n #markAnchor\n [style.position]=\"'absolute'\"\n [fsMenuTriggerFor]=\"markMenu\">\n </span>\n <fs-menu #markMenu>\n @for (action of markMenuActions(); track action) {\n <ng-template\n fs-menu-item\n (click)=\"runMarkAction(action)\">\n {{ markActionLabels[action] }}\n </ng-template>\n }\n </fs-menu>\n}\n<!-- Handles live OUTSIDE the clipped box so they straddle its edges. -->\n@if (editMode && selected) {\n @for (handle of resizeHandles; track handle) {\n <div\n class=\"handle handle-{{ handle }}\"\n (mousedown)=\"stopCanvasPan($event)\"\n (pointerdown)=\"onResizeStart($event, handle)\">\n </div>\n }\n}", styles: [":host{position:absolute;display:flex;flex-direction:column;font-size:10px;--mdc-outlined-button-container-height: 30px;--mdc-outlined-button-label-text-size: 10px;--mat-outlined-button-horizontal-padding: 15px;--mdc-text-button-container-height: 30px;--mdc-text-button-label-text-size: 10px;--mat-text-button-horizontal-padding: 15px;--mat-button-outlined-container-height: 30px;--mat-button-outlined-label-text-size: 10px;--mat-button-outlined-horizontal-padding: 15px;--mat-button-text-container-height: 30px;--mat-button-text-label-text-size: 10px;--mat-button-text-horizontal-padding: 15px}.component-box{position:relative;width:100%;flex:1 1 auto;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;background:#fff;border:1px solid #e4e7eb;border-radius:6px;overflow:hidden;transition:box-shadow .12s ease,border-color .12s ease;cursor:default}.component-box.editing .component-title{cursor:grab}.component-box.editing .component-title:active{cursor:grabbing}.component-box.editing .drill-crumb{pointer-events:none}.component-box.editing:hover{border-color:#9db3c8;box-shadow:0 2px 8px #0f172a14}.component-box.selected{border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color),0 4px 14px #2196f32e}.component-box.text{background:transparent;border-color:transparent;border-radius:0;overflow:visible}.component-box.text.editing{border-style:dashed;border-color:#cbd2d9}.component-box.text.editing:hover{box-shadow:none}.component-box.text.selected{border-style:solid;border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color)}:host ::ng-deep .component-filter .filter-bar-container{align-items:center!important}.component-filter{flex:0 0 auto;display:block;padding:0 0 4px}.component-filter .component-title{display:flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;padding-right:40px;font-size:var(--report-heading-size);font-weight:600;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.component-filter .component-title .truncated-icon{font-size:16px;width:16px;height:16px;color:#f59e0b}.component-filter .component-title .drill-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.component-filter .component-title .drill-crumb{color:inherit;cursor:pointer;text-decoration:none}.component-filter .component-title .drill-crumb:hover,.component-filter .component-title .drill-crumb:focus-visible{text-decoration:underline}.component-filter .component-title .drill-separator{opacity:.6}.component-menu{position:absolute;top:2px;right:2px;z-index:30}.component-body{flex:1 1 auto;min-height:0;position:relative}.component-box.refreshing .component-body,.component-box.refreshing .component-menu{opacity:.35;transition:opacity .15s ease}.load-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));z-index:25;pointer-events:none}:host(.height-auto) .component-state{min-height:120px}.drag-veil{position:absolute;inset:0;cursor:grab;touch-action:none}.drag-veil:active{cursor:grabbing}.component-state{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:12px;color:#7b8794;padding:8px;text-align:center}.component-state.error{color:#e15759}.handle{position:absolute;width:14px;height:14px;display:flex;align-items:center;justify-content:center;touch-action:none;z-index:20;transform:scale(calc(1 / var(--canvas-zoom, 1)))}.handle:before{content:\"\";width:8px;height:8px;background:#fff;border:1.5px solid var(--brand-primary-color);border-radius:2px;box-shadow:0 1px 2px #0f172a33}.handle-nw{left:-7px;top:-7px;cursor:nwse-resize}.handle-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}.handle-ne{right:-7px;top:-7px;cursor:nesw-resize}.handle-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}.handle-se{right:-7px;bottom:-7px;cursor:nwse-resize}.handle-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}.handle-sw{left:-7px;bottom:-7px;cursor:nesw-resize}.handle-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}\n"], dependencies: [{ kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "directive", type: i1$2.FilterHeadingDirective, selector: "[fsFilterHeading]" }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: ComponentChartComponent, selector: "app-report-component-chart", inputs: ["component", "data", "legendSelected", "actions"], outputs: ["legendSelectedChange", "markClick", "markMiss"] }, { kind: "component", type: ComponentGridComponent, selector: "app-report-component-grid", inputs: ["reportId", "component"] }, { kind: "component", type: ComponentKpiComponent, selector: "app-report-component-kpi", inputs: ["component", "data"], outputs: ["markClick"] }, { kind: "component", type: ComponentListComponent, selector: "app-report-component-list", inputs: ["reportId", "component"] }, { kind: "component", type: ComponentTextComponent, selector: "app-report-component-text", inputs: ["html", "tokens", "editing", "shownElsewhere"], outputs: ["saved", "editEnded", "valueSettings"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10339
10705
|
}
|
|
10340
10706
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponentComponent, decorators: [{
|
|
10341
10707
|
type: Component,
|
|
10342
10708
|
args: [{ selector: 'app-report-component', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
|
|
10343
10709
|
MatIcon,
|
|
10344
10710
|
MatIconButton,
|
|
10345
|
-
|
|
10711
|
+
MatProgressSpinner,
|
|
10346
10712
|
MatTooltip,
|
|
10347
10713
|
FsFilterModule,
|
|
10348
10714
|
FsMenuModule,
|
|
10349
10715
|
ComponentChartComponent,
|
|
10716
|
+
ComponentGridComponent,
|
|
10350
10717
|
ComponentKpiComponent,
|
|
10351
10718
|
ComponentListComponent,
|
|
10352
10719
|
ComponentTextComponent,
|
|
10353
|
-
], template: "<div\n class=\"component-box\"\n [class.editing]=\"editMode\"\n [class.text]=\"isText\"\n [class.selected]=\"selected\"\n [style.padding]=\"bodyPadding\"\n (pointerdown)=\"onComponentPointerDown($event)\"\n (mousedown)=\"stopCanvasPan($event)\"\n (touchstart)=\"stopCanvasPan($event)\">\n <!-- The header is an fs-filter: heading template = title (and the edit-mode\n drag handle), actions = the menu, items = component-level filter groups,\n the same strip for every type (lists included). fs-filter reads its\n config once, so the block is keyed on the component-filter set to be\n recreated when it changes. The title shows its tokens filled, and a\n filter change repaints it in place (shownTitle). -->\n <!-- A text block has no title bar \u2014 it IS its words, like a text box on a\n slide \u2014 so it gets neither the header nor the data actions. -->\n @for (key of isText ? [] : [filterKey]; track key) {\n <fs-filter\n class=\"component-filter\"\n [config]=\"filterConfig()\">\n <ng-template fsFilterHeading>\n <div\n class=\"component-title\"\n (pointerdown)=\"onDragStart($event)\">\n <!-- While the chart is drilled the title leads its breadcrumb:\n 'Title \u203A 2026 \u203A Q3', the title as the viewer reads it\n (shownTitle). The title goes back to the top and each\n crumb to the period it names; the last is where the chart is.\n A long path is cut at its left end, so the crumbs nearest the\n chart stay in view. While the layout is edited the crumbs are\n plain text (the stylesheet; drillTo does nothing then). -->\n @if (drillCrumbs().length) {\n <span class=\"drill-path\">\n <span dir=\"ltr\">\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo(0)\"\n (keydown.enter)=\"drillTo(0)\">\n {{ shownTitle }}\n </a>\n @for (label of drillCrumbs(); track $index; let last = $last) {\n <span class=\"drill-separator\">{{ drillSeparator }}</span>\n @if (last) {\n <span>{{ label }}</span>\n } @else {\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo($index + 1)\"\n (keydown.enter)=\"drillTo($index + 1)\">\n {{ label }}\n </a>\n }\n }\n </span>\n </span>\n } @else {\n {{ shownTitle }}\n }\n @if (truncated) {\n <mat-icon\n class=\"truncated-icon\"\n matTooltip=\"Showing a truncated result \u2014 refine the filters to see everything.\">\n warning_amber\n </mat-icon>\n }\n </div>\n </ng-template>\n </fs-filter>\n }\n <!-- The actions menu is floated top-right (absolute) rather than living in\n fs-filter's actions slot, so its ~40px button height no longer drives the\n header row's height. It overlays the box corner; the heading reserves\n right padding so a long title ellipsises before it. -->\n @if (!isText || (editMode && !textEditing())) {\n <button\n mat-icon-button\n class=\"component-menu\"\n [fsMenuTriggerFor]=\"menu\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"stopCanvasPan($event)\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <!-- Beside its trigger, inside the same block: an fs-menu that finds no\n trigger of its own draws one. -->\n <fs-menu #menu>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"editText()\">\n <mat-icon>edit</mat-icon>\n <span>Edit text</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"deleteText()\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillUp()\">\n <mat-icon>arrow_upward</mat-icon>\n <span>Drill up</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillTo(0)\">\n <mat-icon>vertical_align_top</mat-icon>\n <span>Back to top</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"settings()\">\n <mat-icon>tune</mat-icon>\n <span>Settings</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportCsv()\">\n <mat-icon>download</mat-icon>\n <span>Export CSV</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!hidesColumns\"\n (click)=\"chooseColumns()\">\n <mat-icon>view_column</mat-icon>\n <span>Columns</span>\n </ng-template>\n </fs-menu>\n }\n <div class=\"component-body\">\n <!-- A refetch keeps the data on screen under a thin bar; the shimmer\n below is for the first load only. -->\n @if (refreshing()) {\n <mat-progress-bar\n class=\"refresh-bar\"\n aria-label=\"Refreshing data\"\n [mode]=\"'indeterminate'\">\n </mat-progress-bar>\n }\n @if (isText) {\n <app-report-component-text\n [html]=\"textHtml() ?? component.config?.html ?? ''\"\n [tokens]=\"tokens()\"\n [editing]=\"textEditing()\"\n [shownElsewhere]=\"shownElsewhere\"\n (saved)=\"onTextSaved($event)\"\n (editEnded)=\"textEditing.set(false)\"\n (valueSettings)=\"valueSettings.emit($event)\">\n </app-report-component-text>\n } @else if (component.type === 'list') {\n @for (key of [listResetKey()]; track key) {\n <app-report-component-list\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-list>\n }\n } @else if (loading()) {\n <div class=\"component-state\">\n <div class=\"loading-shimmer\"></div>\n </div>\n } @else if (error()) {\n <div class=\"component-state error\">\n {{ error() }}\n </div>\n } @else if (!data()?.rows?.length) {\n <div class=\"component-state\">\n {{ emptyMessage() }}\n </div>\n } @else if (component.type === 'kpi') {\n <app-report-component-kpi\n [component]=\"component\"\n [data]=\"data()\"\n (markClick)=\"onMarkClick($event)\">\n </app-report-component-kpi>\n } @else {\n <app-report-component-chart\n [component]=\"component\"\n [data]=\"data()\"\n [actions]=\"tileActions()\"\n [(legendSelected)]=\"legendSelected\"\n (markClick)=\"onMarkClick($event)\"\n (markMiss)=\"onMarkMiss()\">\n </app-report-component-chart>\n }\n <!-- In edit mode a transparent veil over the body makes the WHOLE\n component draggable (industry standard: you grab the object, not just\n its title bar) and keeps inner widgets from swallowing the gesture. -->\n @if (editMode && !textEditing()) {\n <div\n class=\"drag-veil\"\n (pointerdown)=\"onDragStart($event)\">\n </div>\n }\n </div>\n</div>\n<!-- The mark menu, for a click whose mark offers two or more actions. It\n opens at the click: its trigger is a zero-size anchor that\n anchor-at-point.ts moves there first. -->\n@if (!isText && !editMode) {\n <span\n #markAnchor\n [style.position]=\"'absolute'\"\n [fsMenuTriggerFor]=\"markMenu\">\n </span>\n <fs-menu #markMenu>\n @for (action of markMenuActions(); track action) {\n <ng-template\n fs-menu-item\n (click)=\"runMarkAction(action)\">\n {{ markActionLabels[action] }}\n </ng-template>\n }\n </fs-menu>\n}\n<!-- Handles live OUTSIDE the clipped box so they straddle its edges. -->\n@if (editMode && selected) {\n @for (handle of resizeHandles; track handle) {\n <div\n class=\"handle handle-{{ handle }}\"\n (mousedown)=\"stopCanvasPan($event)\"\n (pointerdown)=\"onResizeStart($event, handle)\">\n </div>\n }\n}", styles: [":host{position:absolute;display:flex;flex-direction:column;font-size:10px;--mdc-outlined-button-container-height: 30px;--mdc-outlined-button-label-text-size: 10px;--mat-outlined-button-horizontal-padding: 15px;--mdc-text-button-container-height: 30px;--mdc-text-button-label-text-size: 10px;--mat-text-button-horizontal-padding: 15px;--mat-button-outlined-container-height: 30px;--mat-button-outlined-label-text-size: 10px;--mat-button-outlined-horizontal-padding: 15px;--mat-button-text-container-height: 30px;--mat-button-text-label-text-size: 10px;--mat-button-text-horizontal-padding: 15px}.component-box{position:relative;width:100%;flex:1 1 auto;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;background:#fff;border:1px solid #e4e7eb;border-radius:6px;overflow:hidden;transition:box-shadow .12s ease,border-color .12s ease;cursor:default}.component-box.editing .component-title{cursor:grab}.component-box.editing .component-title:active{cursor:grabbing}.component-box.editing .drill-crumb{pointer-events:none}.component-box.editing:hover{border-color:#9db3c8;box-shadow:0 2px 8px #0f172a14}.component-box.selected{border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color),0 4px 14px #2196f32e}.component-box.text{background:transparent;border-color:transparent;border-radius:0;overflow:visible}.component-box.text.editing{border-style:dashed;border-color:#cbd2d9}.component-box.text.editing:hover{box-shadow:none}.component-box.text.selected{border-style:solid;border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color)}:host ::ng-deep .component-filter .filter-bar-container{align-items:center!important}.component-filter{flex:0 0 auto;display:block;padding:0 0 4px}.component-filter .component-title{display:flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;padding-right:40px;font-size:var(--report-heading-size);font-weight:600;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.component-filter .component-title .truncated-icon{font-size:16px;width:16px;height:16px;color:#f59e0b}.component-filter .component-title .drill-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.component-filter .component-title .drill-crumb{color:inherit;cursor:pointer;text-decoration:none}.component-filter .component-title .drill-crumb:hover,.component-filter .component-title .drill-crumb:focus-visible{text-decoration:underline}.component-filter .component-title .drill-separator{opacity:.6}.component-menu{position:absolute;top:2px;right:2px;z-index:30}.component-body{flex:1 1 auto;min-height:0;position:relative}.refresh-bar{position:absolute;top:0;left:0;right:0;z-index:1;pointer-events:none;--mdc-linear-progress-track-height: 2px;--mdc-linear-progress-active-indicator-height: 2px;--mat-progress-bar-track-height: 2px;--mat-progress-bar-active-indicator-height: 2px}:host(.height-auto) .component-state{min-height:120px}.drag-veil{position:absolute;inset:0;cursor:grab;touch-action:none}.drag-veil:active{cursor:grabbing}.component-state{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:12px;color:#7b8794;padding:8px;text-align:center}.component-state.error{color:#e15759}.loading-shimmer{width:70%;height:60%;border-radius:6px;background:linear-gradient(100deg,#f0f4f8 40%,#e4ebf2,#f0f4f8 60%);background-size:200% 100%;animation:shimmer 1.2s infinite linear}@keyframes shimmer{to{background-position-x:-200%}}.handle{position:absolute;width:14px;height:14px;display:flex;align-items:center;justify-content:center;touch-action:none;z-index:20;transform:scale(calc(1 / var(--canvas-zoom, 1)))}.handle:before{content:\"\";width:8px;height:8px;background:#fff;border:1.5px solid var(--brand-primary-color);border-radius:2px;box-shadow:0 1px 2px #0f172a33}.handle-nw{left:-7px;top:-7px;cursor:nwse-resize}.handle-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}.handle-ne{right:-7px;top:-7px;cursor:nesw-resize}.handle-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}.handle-se{right:-7px;bottom:-7px;cursor:nwse-resize}.handle-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}.handle-sw{left:-7px;bottom:-7px;cursor:nesw-resize}.handle-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}\n"] }]
|
|
10720
|
+
], template: "<div\n class=\"component-box\"\n [class.editing]=\"editMode\"\n [class.text]=\"isText\"\n [class.selected]=\"selected\"\n [class.refreshing]=\"refreshing()\"\n [style.padding]=\"bodyPadding\"\n (pointerdown)=\"onComponentPointerDown($event)\"\n (mousedown)=\"stopCanvasPan($event)\"\n (touchstart)=\"stopCanvasPan($event)\">\n <!-- The header is an fs-filter: heading template = title (and the edit-mode\n drag handle), actions = the menu, items = component-level filter groups,\n the same strip for every type (lists included). fs-filter reads its\n config once, so the block is keyed on the component-filter set to be\n recreated when it changes. The title shows its tokens filled, and a\n filter change repaints it in place (shownTitle). -->\n <!-- A text block has no title bar \u2014 it IS its words, like a text box on a\n slide \u2014 so it gets neither the header nor the data actions. -->\n @for (key of isText ? [] : [filterKey]; track key) {\n <fs-filter\n class=\"component-filter\"\n [config]=\"filterConfig()\">\n <ng-template fsFilterHeading>\n <div\n class=\"component-title\"\n (pointerdown)=\"onDragStart($event)\">\n <!-- While the chart is drilled the title leads its breadcrumb:\n 'Title \u203A 2026 \u203A Q3', the title as the viewer reads it\n (shownTitle). The title goes back to the top and each\n crumb to the period it names; the last is where the chart is.\n A long path is cut at its left end, so the crumbs nearest the\n chart stay in view. While the layout is edited the crumbs are\n plain text (the stylesheet; drillTo does nothing then). -->\n @if (drillCrumbs().length) {\n <span class=\"drill-path\">\n <span dir=\"ltr\">\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo(0)\"\n (keydown.enter)=\"drillTo(0)\">\n {{ shownTitle }}\n </a>\n @for (label of drillCrumbs(); track $index; let last = $last) {\n <span class=\"drill-separator\">{{ drillSeparator }}</span>\n @if (last) {\n <span>{{ label }}</span>\n } @else {\n <a\n class=\"drill-crumb\"\n tabindex=\"0\"\n (click)=\"drillTo($index + 1)\"\n (keydown.enter)=\"drillTo($index + 1)\">\n {{ label }}\n </a>\n }\n }\n </span>\n </span>\n } @else {\n {{ shownTitle }}\n }\n @if (truncated) {\n <mat-icon\n class=\"truncated-icon\"\n matTooltip=\"Showing a truncated result \u2014 refine the filters to see everything.\">\n warning_amber\n </mat-icon>\n }\n </div>\n </ng-template>\n </fs-filter>\n }\n <!-- The actions menu is floated top-right (absolute) rather than living in\n fs-filter's actions slot, so its ~40px button height no longer drives the\n header row's height. It overlays the box corner; the heading reserves\n right padding so a long title ellipsises before it. -->\n @if (!isText || (editMode && !textEditing())) {\n <button\n mat-icon-button\n class=\"component-menu\"\n [fsMenuTriggerFor]=\"menu\"\n (pointerdown)=\"$event.stopPropagation()\"\n (mousedown)=\"stopCanvasPan($event)\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <!-- Beside its trigger, inside the same block: an fs-menu that finds no\n trigger of its own draws one. -->\n <fs-menu #menu>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"editText()\">\n <mat-icon>edit</mat-icon>\n <span>Edit text</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!isText\"\n (click)=\"deleteText()\">\n <mat-icon>delete</mat-icon>\n <span>Delete</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillUp()\">\n <mat-icon>arrow_upward</mat-icon>\n <span>Drill up</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!drillCrumbs().length || editMode\"\n (click)=\"drillTo(0)\">\n <mat-icon>vertical_align_top</mat-icon>\n <span>Back to top</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"settings()\">\n <mat-icon>tune</mat-icon>\n <span>Settings</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"isText\"\n (click)=\"exportCsv()\">\n <mat-icon>download</mat-icon>\n <span>Export CSV</span>\n </ng-template>\n <ng-template\n fs-menu-item\n [hidden]=\"!hidesColumns\"\n (click)=\"chooseColumns()\">\n <mat-icon>view_column</mat-icon>\n <span>Columns</span>\n </ng-template>\n </fs-menu>\n }\n <div class=\"component-body\">\n @if (isText) {\n <app-report-component-text\n [html]=\"textHtml() ?? component.config?.html ?? ''\"\n [tokens]=\"tokens()\"\n [editing]=\"textEditing()\"\n [shownElsewhere]=\"shownElsewhere\"\n (saved)=\"onTextSaved($event)\"\n (editEnded)=\"textEditing.set(false)\"\n (valueSettings)=\"valueSettings.emit($event)\">\n </app-report-component-text>\n } @else if (component.type === 'list') {\n @for (key of [listResetKey()]; track key) {\n <app-report-component-list\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-list>\n }\n } @else if (component.type === 'grid') {\n <app-report-component-grid\n [reportId]=\"reportId\"\n [component]=\"component\">\n </app-report-component-grid>\n } @else if (loading()) {\n <!-- Holds the tile's height for the spinner on the first load. -->\n <div class=\"component-state\"></div>\n } @else if (error()) {\n <div class=\"component-state error\">\n {{ error() }}\n </div>\n } @else if (!data()?.rows?.length) {\n <div class=\"component-state\">\n {{ emptyMessage() }}\n </div>\n } @else if (component.type === 'kpi') {\n <app-report-component-kpi\n [component]=\"component\"\n [data]=\"data()\"\n (markClick)=\"onMarkClick($event)\">\n </app-report-component-kpi>\n } @else {\n <app-report-component-chart\n [component]=\"component\"\n [data]=\"data()\"\n [actions]=\"tileActions()\"\n [(legendSelected)]=\"legendSelected\"\n (markClick)=\"onMarkClick($event)\"\n (markMiss)=\"onMarkMiss()\">\n </app-report-component-chart>\n }\n <!-- In edit mode a transparent veil over the body makes the WHOLE\n component draggable (industry standard: you grab the object, not just\n its title bar) and keeps inner widgets from swallowing the gesture. -->\n @if (editMode && !textEditing()) {\n <div\n class=\"drag-veil\"\n (pointerdown)=\"onDragStart($event)\">\n </div>\n }\n </div>\n <!-- A load: a spinner centred on the whole tile. On a refetch the body and\n the menu are dimmed under it (.refreshing); the title is not. -->\n @if (loading() || refreshing()) {\n <mat-progress-spinner\n class=\"load-spinner\"\n aria-label=\"Loading data\"\n mode=\"indeterminate\"\n [diameter]=\"15\">\n </mat-progress-spinner>\n }\n</div>\n<!-- The mark menu, for a click whose mark offers two or more actions. It\n opens at the click: its trigger is a zero-size anchor that\n anchor-at-point.ts moves there first. -->\n@if (!isText && !editMode) {\n <span\n #markAnchor\n [style.position]=\"'absolute'\"\n [fsMenuTriggerFor]=\"markMenu\">\n </span>\n <fs-menu #markMenu>\n @for (action of markMenuActions(); track action) {\n <ng-template\n fs-menu-item\n (click)=\"runMarkAction(action)\">\n {{ markActionLabels[action] }}\n </ng-template>\n }\n </fs-menu>\n}\n<!-- Handles live OUTSIDE the clipped box so they straddle its edges. -->\n@if (editMode && selected) {\n @for (handle of resizeHandles; track handle) {\n <div\n class=\"handle handle-{{ handle }}\"\n (mousedown)=\"stopCanvasPan($event)\"\n (pointerdown)=\"onResizeStart($event, handle)\">\n </div>\n }\n}", styles: [":host{position:absolute;display:flex;flex-direction:column;font-size:10px;--mdc-outlined-button-container-height: 30px;--mdc-outlined-button-label-text-size: 10px;--mat-outlined-button-horizontal-padding: 15px;--mdc-text-button-container-height: 30px;--mdc-text-button-label-text-size: 10px;--mat-text-button-horizontal-padding: 15px;--mat-button-outlined-container-height: 30px;--mat-button-outlined-label-text-size: 10px;--mat-button-outlined-horizontal-padding: 15px;--mat-button-text-container-height: 30px;--mat-button-text-label-text-size: 10px;--mat-button-text-horizontal-padding: 15px}.component-box{position:relative;width:100%;flex:1 1 auto;min-height:0;box-sizing:border-box;display:flex;flex-direction:column;background:#fff;border:1px solid #e4e7eb;border-radius:6px;overflow:hidden;transition:box-shadow .12s ease,border-color .12s ease;cursor:default}.component-box.editing .component-title{cursor:grab}.component-box.editing .component-title:active{cursor:grabbing}.component-box.editing .drill-crumb{pointer-events:none}.component-box.editing:hover{border-color:#9db3c8;box-shadow:0 2px 8px #0f172a14}.component-box.selected{border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color),0 4px 14px #2196f32e}.component-box.text{background:transparent;border-color:transparent;border-radius:0;overflow:visible}.component-box.text.editing{border-style:dashed;border-color:#cbd2d9}.component-box.text.editing:hover{box-shadow:none}.component-box.text.selected{border-style:solid;border-color:var(--brand-primary-color);box-shadow:0 0 0 1px var(--brand-primary-color)}:host ::ng-deep .component-filter .filter-bar-container{align-items:center!important}.component-filter{flex:0 0 auto;display:block;padding:0 0 4px}.component-filter .component-title{display:flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;padding-right:40px;font-size:var(--report-heading-size);font-weight:600;color:#1f2933;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.component-filter .component-title .truncated-icon{font-size:16px;width:16px;height:16px;color:#f59e0b}.component-filter .component-title .drill-path{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.component-filter .component-title .drill-crumb{color:inherit;cursor:pointer;text-decoration:none}.component-filter .component-title .drill-crumb:hover,.component-filter .component-title .drill-crumb:focus-visible{text-decoration:underline}.component-filter .component-title .drill-separator{opacity:.6}.component-menu{position:absolute;top:2px;right:2px;z-index:30}.component-body{flex:1 1 auto;min-height:0;position:relative}.component-box.refreshing .component-body,.component-box.refreshing .component-menu{opacity:.35;transition:opacity .15s ease}.load-spinner{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(calc(1 / var(--canvas-zoom, 1)));z-index:25;pointer-events:none}:host(.height-auto) .component-state{min-height:120px}.drag-veil{position:absolute;inset:0;cursor:grab;touch-action:none}.drag-veil:active{cursor:grabbing}.component-state{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:12px;color:#7b8794;padding:8px;text-align:center}.component-state.error{color:#e15759}.handle{position:absolute;width:14px;height:14px;display:flex;align-items:center;justify-content:center;touch-action:none;z-index:20;transform:scale(calc(1 / var(--canvas-zoom, 1)))}.handle:before{content:\"\";width:8px;height:8px;background:#fff;border:1.5px solid var(--brand-primary-color);border-radius:2px;box-shadow:0 1px 2px #0f172a33}.handle-nw{left:-7px;top:-7px;cursor:nwse-resize}.handle-n{left:calc(50% - 7px);top:-7px;cursor:ns-resize}.handle-ne{right:-7px;top:-7px;cursor:nesw-resize}.handle-e{right:-7px;top:calc(50% - 7px);cursor:ew-resize}.handle-se{right:-7px;bottom:-7px;cursor:nwse-resize}.handle-s{left:calc(50% - 7px);bottom:-7px;cursor:ns-resize}.handle-sw{left:-7px;bottom:-7px;cursor:nesw-resize}.handle-w{left:-7px;top:calc(50% - 7px);cursor:ew-resize}\n"] }]
|
|
10354
10721
|
}], propDecorators: { reportId: [{
|
|
10355
10722
|
type: Input
|
|
10356
10723
|
}], component: [{
|
|
@@ -13308,7 +13675,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
13308
13675
|
// in the report's format context, as on screen.
|
|
13309
13676
|
async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
|
|
13310
13677
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
13311
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
13678
|
+
const echarts = (await import('./firestitch-report-echarts-CoaGXKET.mjs')).default;
|
|
13312
13679
|
const host = document.createElement('div');
|
|
13313
13680
|
host.style.width = `${width}px`;
|
|
13314
13681
|
host.style.height = `${height}px`;
|
|
@@ -16441,7 +16808,7 @@ class ReportComponent {
|
|
|
16441
16808
|
ReportExportService,
|
|
16442
16809
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
16443
16810
|
// this route's chunk.
|
|
16444
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
16811
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CoaGXKET.mjs').then((module) => module.default) }),
|
|
16445
16812
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
16446
16813
|
// presets; a preset only sets the days.
|
|
16447
16814
|
provideReportDatePicker(),
|
|
@@ -16462,7 +16829,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
16462
16829
|
ReportExportService,
|
|
16463
16830
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
16464
16831
|
// this route's chunk.
|
|
16465
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
16832
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CoaGXKET.mjs').then((module) => module.default) }),
|
|
16466
16833
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
16467
16834
|
// presets; a preset only sets the days.
|
|
16468
16835
|
provideReportDatePicker(),
|
|
@@ -17673,11 +18040,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
17673
18040
|
type: Injectable
|
|
17674
18041
|
}] });
|
|
17675
18042
|
|
|
17676
|
-
// The parts of the chat's viewer state (ReportChatViewerState) read off a
|
|
17677
|
-
// snapshot of the screen (the state service's snapshot()). Pure, so the
|
|
17678
|
-
// node harness checks them. The caps are the server's: it reads at most
|
|
17679
|
-
// CHAT_DRILLS_MAX drilled charts, DRILL_MAX_DEPTH steps a path and
|
|
17680
|
-
// CHAT_LABEL_MAX characters a label, and drops the rest.
|
|
17681
18043
|
const CHAT_DRILLS_MAX = 10;
|
|
17682
18044
|
const CHAT_LABEL_MAX = 100;
|
|
17683
18045
|
// The match the chat is told of: 'any' while the groups match any filter.
|
|
@@ -17704,6 +18066,26 @@ function chatViewerDrills(snapshot, locale) {
|
|
|
17704
18066
|
})),
|
|
17705
18067
|
}));
|
|
17706
18068
|
}
|
|
18069
|
+
// Every table tile on screen while `pageId` is the page on screen: the
|
|
18070
|
+
// page's own, then the header and footer's unless the page hides them, each
|
|
18071
|
+
// with the layout the viewer has it in (layoutOf; null while it shows the
|
|
18072
|
+
// stored layout, and then left out). Empty when the page is unknown or holds
|
|
18073
|
+
// no table.
|
|
18074
|
+
function chatViewerTiles(report, pageId, layoutOf) {
|
|
18075
|
+
const page = (report?.pages ?? []).find((item) => item.id === pageId);
|
|
18076
|
+
if (!report || !page) {
|
|
18077
|
+
return [];
|
|
18078
|
+
}
|
|
18079
|
+
return [
|
|
18080
|
+
...(page.components ?? []),
|
|
18081
|
+
...(page.masterHidden ? [] : report.master?.components ?? []),
|
|
18082
|
+
]
|
|
18083
|
+
.filter((component) => component.type === 'grid')
|
|
18084
|
+
.map((component) => {
|
|
18085
|
+
const layout = layoutOf(component);
|
|
18086
|
+
return layout ? { componentId: component.id, layout } : { componentId: component.id };
|
|
18087
|
+
});
|
|
18088
|
+
}
|
|
17707
18089
|
|
|
17708
18090
|
// The groups a saved filter holds: the report-wide ones.
|
|
17709
18091
|
function savedFilterGroups(groups) {
|
|
@@ -17804,12 +18186,12 @@ class FsAiReportComponent {
|
|
|
17804
18186
|
reportId = input(null);
|
|
17805
18187
|
// API base for the reports + chat endpoints; override to mount elsewhere.
|
|
17806
18188
|
basePath = input('reports');
|
|
17807
|
-
// The query param
|
|
17808
|
-
//
|
|
17809
|
-
//
|
|
17810
|
-
// the
|
|
17811
|
-
// (ReportFilterStateService)
|
|
17812
|
-
//
|
|
18189
|
+
// The query param that carries the page on screen (?page=7). A report opens
|
|
18190
|
+
// on the page it names, and each move to another page writes it, in place,
|
|
18191
|
+
// so a refresh or a shared link comes back to the same page. It is the only
|
|
18192
|
+
// state a report keeps in the URL: its filters and the rest live in the
|
|
18193
|
+
// report's state (ReportFilterStateService). A host that mounts several
|
|
18194
|
+
// reports on one route, or wants no URL writes, sets this to null.
|
|
17813
18195
|
pageQueryParam = input('page');
|
|
17814
18196
|
// The report was deleted from its settings dialog — the host routes back to
|
|
17815
18197
|
// the listing.
|
|
@@ -17893,6 +18275,7 @@ class FsAiReportComponent {
|
|
|
17893
18275
|
_reportData = inject(ReportData);
|
|
17894
18276
|
_reportAiChatData = inject(ReportAiChatData);
|
|
17895
18277
|
_route = inject(ActivatedRoute);
|
|
18278
|
+
_router = inject(Router);
|
|
17896
18279
|
_filterState = inject(ReportFilterStateService);
|
|
17897
18280
|
_filterOptions = inject(ReportFilterOptionsService);
|
|
17898
18281
|
_exportService = inject(ReportExportService);
|
|
@@ -18046,7 +18429,7 @@ class FsAiReportComponent {
|
|
|
18046
18429
|
});
|
|
18047
18430
|
}
|
|
18048
18431
|
// The canvas moved to another page. The report-level filters stay as they
|
|
18049
|
-
// are (they live in the state service)
|
|
18432
|
+
// are (they live in the state service); the page goes into the URL.
|
|
18050
18433
|
onCanvasPageChanged(pageId) {
|
|
18051
18434
|
// The asked-for page has been consumed; from here the canvas owns the page.
|
|
18052
18435
|
this.pageId.set(null);
|
|
@@ -18054,6 +18437,7 @@ class FsAiReportComponent {
|
|
|
18054
18437
|
// The open page is part of the state the tiles and exports read.
|
|
18055
18438
|
this._filterState.setPage(pageId);
|
|
18056
18439
|
this.pageChanged.emit(pageId);
|
|
18440
|
+
this._writePageToUrl(pageId);
|
|
18057
18441
|
// The bar shows the page groups of the page on screen.
|
|
18058
18442
|
if (hasPageGroup(this.report())) {
|
|
18059
18443
|
this._buildReportConfig();
|
|
@@ -18150,16 +18534,20 @@ class FsAiReportComponent {
|
|
|
18150
18534
|
// What the viewer has on screen as a chat turn sends it. The canvas
|
|
18151
18535
|
// selection counts only in edit mode: it is drawn only then, and it is kept
|
|
18152
18536
|
// (not cleared) when edit mode is left. The match is sent only while it is
|
|
18153
|
-
// 'any',
|
|
18537
|
+
// 'any', the drilled charts only when one is drilled, and the tables only
|
|
18538
|
+
// when the bound page shows one, each with the layout the viewer has it in
|
|
18539
|
+
// while that differs from the stored one.
|
|
18154
18540
|
_chatViewerState() {
|
|
18155
18541
|
const snapshot = this._filterState.snapshot();
|
|
18156
18542
|
const drills = chatViewerDrills(snapshot, this._filterState.formatContext().locale);
|
|
18543
|
+
const tiles = chatViewerTiles(this._filterState.report(), this.chatPageId(), (component) => this._filterState.gridSessionLayout(component));
|
|
18157
18544
|
return {
|
|
18158
18545
|
granularity: this._filterState.frequency(),
|
|
18159
18546
|
selectedComponentId: this.editMode() ? this._canvas?.selectedComponentId() ?? null : null,
|
|
18160
18547
|
filters: this._filterState.viewerFilters(),
|
|
18161
18548
|
...chatViewerMatch(snapshot),
|
|
18162
18549
|
...(drills.length ? { drills } : {}),
|
|
18550
|
+
...(tiles.length ? { tiles } : {}),
|
|
18163
18551
|
};
|
|
18164
18552
|
}
|
|
18165
18553
|
_resizeTo(clientX) {
|
|
@@ -18171,8 +18559,25 @@ class FsAiReportComponent {
|
|
|
18171
18559
|
const clamped = Math.min(0.8, Math.max(0.2, ratio));
|
|
18172
18560
|
this._chatPanel.nativeElement.style.flexBasis = `${clamped * 100}%`;
|
|
18173
18561
|
}
|
|
18174
|
-
// The page
|
|
18175
|
-
//
|
|
18562
|
+
// The page on screen into the URL (?page=), replacing the current history
|
|
18563
|
+
// entry rather than adding one, so Back leaves the report instead of
|
|
18564
|
+
// stepping through its pages. Only the page param is touched; the host's
|
|
18565
|
+
// own query params stay. Nothing is written when the param is off, or when
|
|
18566
|
+
// the URL already names the page.
|
|
18567
|
+
_writePageToUrl(pageId) {
|
|
18568
|
+
const param = this.pageQueryParam();
|
|
18569
|
+
if (!param || !pageId || this._pageIdFromUrl() === pageId) {
|
|
18570
|
+
return;
|
|
18571
|
+
}
|
|
18572
|
+
this._router.navigate([], {
|
|
18573
|
+
relativeTo: this._route,
|
|
18574
|
+
queryParams: { [param]: pageId },
|
|
18575
|
+
queryParamsHandling: 'merge',
|
|
18576
|
+
replaceUrl: true,
|
|
18577
|
+
});
|
|
18578
|
+
}
|
|
18579
|
+
// The page id the URL holds right now (?page=), or null when the page param
|
|
18580
|
+
// is off or the value is not a usable id.
|
|
18176
18581
|
_pageIdFromUrl() {
|
|
18177
18582
|
const param = this.pageQueryParam();
|
|
18178
18583
|
if (!param) {
|
|
@@ -18400,7 +18805,7 @@ class FsAiReportComponent {
|
|
|
18400
18805
|
ReportAiChatData,
|
|
18401
18806
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
18402
18807
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
18403
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
18808
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CoaGXKET.mjs').then((module) => module.default) }),
|
|
18404
18809
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
18405
18810
|
// presets; a preset only sets the days.
|
|
18406
18811
|
provideReportDatePicker(),
|
|
@@ -18424,7 +18829,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
18424
18829
|
ReportAiChatData,
|
|
18425
18830
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
18426
18831
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
18427
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
18832
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CoaGXKET.mjs').then((module) => module.default) }),
|
|
18428
18833
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
18429
18834
|
// presets; a preset only sets the days.
|
|
18430
18835
|
provideReportDatePicker(),
|
|
@@ -18464,4 +18869,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
18464
18869
|
*/
|
|
18465
18870
|
|
|
18466
18871
|
export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, REPORT_CHART_FONT_FAMILY as a, REPORT_AXIS_LABEL_FONT_SIZE as b, ReportComponent as c, FsAiReportComponent as d, ReportData as e, ReportService as f, ReportFilterStateService as g, ReportNodeType as h, ReportState as i, ReportValueFormat as j, ReportValueFormula as k, ReportListColumnFormat as l, ReportNumberUnits as m, ReportChartDataLabels as n, ReportChartColor as o, ReportChartLineStyle as p, ReportChartMarkStat as q, ReportChartDateLabel as r, ReportChangePolarity as s };
|
|
18467
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
18872
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-CHPA7sla.mjs.map
|