@firestitch/report 18.12.0 → 18.14.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 +9 -1
- package/app/reports/components/component-grid/report-grid-cell.pipe.d.ts +11 -0
- package/app/reports/components/report-component/report-component.component.d.ts +0 -1
- package/app/reports/data/report-grid-source.d.ts +1 -0
- package/app/reports/export/report-export-collector.service.d.ts +8 -1
- package/app/reports/export/report-filter-lines.d.ts +2 -0
- package/app/reports/export/report-grid-export.d.ts +20 -0
- package/app/reports/export/report-pdf.service.d.ts +2 -0
- package/app/reports/export/report-pptx.service.d.ts +1 -0
- package/app/reports/group-by.d.ts +3 -0
- package/app/reports/interfaces/{report-chat-viewer-detail.interface.d.ts → report-chat-viewer-show-rows.interface.d.ts} +1 -1
- package/app/reports/interfaces/report-chat-viewer-state.interface.d.ts +2 -2
- package/app/reports/interfaces/report-export-snapshot.interface.d.ts +2 -0
- package/app/reports/interfaces/report-mark-action.type.d.ts +1 -1
- package/app/reports/interfaces/report-temporary-state.interface.d.ts +1 -1
- package/app/reports/interfaces/report-viewer-feature.type.d.ts +1 -1
- package/app/reports/interfaces/report.interface.d.ts +7 -0
- package/app/reports/report-filter-items.d.ts +1 -1
- package/app/reports/report-grid-layout.d.ts +9 -2
- package/app/reports/services/report-filter-state.service.d.ts +8 -1
- package/app/reports/tile-data-state.d.ts +2 -2
- package/esm2022/app/reports/components/component-grid/component-grid.component.mjs +70 -8
- package/esm2022/app/reports/components/component-grid/report-grid-cell.pipe.mjs +41 -0
- package/esm2022/app/reports/components/report-component/report-component.component.mjs +15 -19
- package/esm2022/app/reports/data/report-grid-source.mjs +11 -2
- package/esm2022/app/reports/data/report.data.mjs +3 -3
- package/esm2022/app/reports/export/report-export-collector.service.mjs +72 -7
- package/esm2022/app/reports/export/report-filter-lines.mjs +68 -1
- package/esm2022/app/reports/export/report-grid-export.mjs +48 -0
- package/esm2022/app/reports/export/report-pdf.service.mjs +47 -17
- package/esm2022/app/reports/export/report-pptx.service.mjs +48 -4
- package/esm2022/app/reports/group-by.mjs +13 -0
- package/esm2022/app/reports/interfaces/report-chat-viewer-show-rows.interface.mjs +2 -0
- package/esm2022/app/reports/interfaces/report-chat-viewer-state.interface.mjs +1 -1
- package/esm2022/app/reports/interfaces/report-export-snapshot.interface.mjs +1 -1
- package/esm2022/app/reports/interfaces/report-mark-action.type.mjs +1 -1
- package/esm2022/app/reports/interfaces/report-temporary-state.interface.mjs +1 -1
- package/esm2022/app/reports/interfaces/report-viewer-feature.type.mjs +1 -1
- package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
- package/esm2022/app/reports/mark-click.mjs +2 -2
- package/esm2022/app/reports/option-builder.mjs +4 -4
- package/esm2022/app/reports/report-filter-items.mjs +9 -7
- package/esm2022/app/reports/report-grid-layout.mjs +98 -4
- package/esm2022/app/reports/report-viewer-features.mjs +4 -2
- package/esm2022/app/reports/services/report-filter-state.service.mjs +125 -39
- package/esm2022/app/reports/tile-data-state.mjs +12 -17
- package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +2 -2
- package/esm2022/app/reports/views/report/report.component.mjs +2 -2
- package/fesm2022/{firestitch-report-echarts-CoaGXKET.mjs → firestitch-report-echarts-DMflI6jp.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-CoaGXKET.mjs.map → firestitch-report-echarts-DMflI6jp.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-CHPA7sla.mjs → firestitch-report-firestitch-report-C94rjx24.mjs} +660 -125
- package/fesm2022/firestitch-report-firestitch-report-C94rjx24.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +2 -2
- package/esm2022/app/reports/interfaces/report-chat-viewer-detail.interface.mjs +0 -2
- package/fesm2022/firestitch-report-firestitch-report-CHPA7sla.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, signal, computed, DestroyRef, Component, ChangeDetectionStrategy, input, model, output, untracked, NgZone, Pipe,
|
|
2
|
+
import { inject, Injectable, signal, computed, DestroyRef, Component, ChangeDetectionStrategy, input, model, output, untracked, NgZone, Pipe, Injector, effect, viewChild, ElementRef, afterNextRender, EventEmitter, Input, Output, ViewChild, HostBinding, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
3
3
|
import * as i1$1 from '@angular/forms';
|
|
4
4
|
import { FormsModule, ControlContainer, NgForm } from '@angular/forms';
|
|
5
5
|
import * as i3$3 from '@angular/material/button';
|
|
@@ -29,20 +29,20 @@ import * as i3$1 from '@firestitch/zoom-pan';
|
|
|
29
29
|
import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
|
|
30
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
31
|
import { FsApi, RequestMethod, StreamEventType, DisplayApiError } from '@firestitch/api';
|
|
32
|
+
import { GridFieldType, GridGranularity, canonicalGridLayout, convertDateFilter, GridNoteCode, GridControlState, GridFormat, GridUnits, FsGridComponent, FsGridCellDirective, GridRowKind, readGridTable } from '@firestitch/grid';
|
|
32
33
|
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
34
|
import { toZonedTime } from 'date-fns-tz';
|
|
34
|
-
import { canonicalGridLayout, GridFormat, GridUnits, GridFieldType, FsGridComponent } from '@firestitch/grid';
|
|
35
35
|
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
|
36
36
|
import { MatCheckbox } from '@angular/material/checkbox';
|
|
37
37
|
import * as i3 from '@firestitch/dialog';
|
|
38
38
|
import { FsDialogModule } from '@firestitch/dialog';
|
|
39
|
+
import { FsPopoverDirective } from '@firestitch/popover';
|
|
39
40
|
import { HttpErrorResponse, HttpContext } from '@angular/common/http';
|
|
40
41
|
import { NgTemplateOutlet, DOCUMENT } from '@angular/common';
|
|
41
42
|
import * as i1 from '@firestitch/list';
|
|
42
43
|
import { FsListComponent, PaginationStrategy, FsListModule } from '@firestitch/list';
|
|
43
44
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
44
45
|
import { Plugin, FsHtmlEditorComponent } from '@firestitch/html-editor';
|
|
45
|
-
import { FsPopoverDirective } from '@firestitch/popover';
|
|
46
46
|
import { MatFormField, MatHint, MatLabel, MatSuffix } from '@angular/material/form-field';
|
|
47
47
|
import { MatInput } from '@angular/material/input';
|
|
48
48
|
import { MatOption, MatSelect } from '@angular/material/select';
|
|
@@ -114,7 +114,8 @@ function canvasLandingPage(input) {
|
|
|
114
114
|
// cascadingOptions narrows a select's list by the other filters of its tile
|
|
115
115
|
// (ReportFilterOptionsService). drill is a time chart's click down, with
|
|
116
116
|
// the tile header's breadcrumb and its menu's Drill up and Back to top as the
|
|
117
|
-
// way back.
|
|
117
|
+
// way back. grid is a table tile drawn by fs-grid: a server that serves it
|
|
118
|
+
// sends a table only to a viewer that names it.
|
|
118
119
|
const VIEWER_FEATURES = [
|
|
119
120
|
'scopeFilters',
|
|
120
121
|
'excludeValues',
|
|
@@ -123,6 +124,7 @@ const VIEWER_FEATURES = [
|
|
|
123
124
|
'pageScope',
|
|
124
125
|
'cascadingOptions',
|
|
125
126
|
'drill',
|
|
127
|
+
'grid',
|
|
126
128
|
];
|
|
127
129
|
// The features a report offers on this screen: the ones its server serves
|
|
128
130
|
// (Report.viewerFeatures) that this viewer also implements.
|
|
@@ -392,8 +394,8 @@ class ReportData {
|
|
|
392
394
|
// resolves to a presigned URL for a zip of one CSV per component, foldered by
|
|
393
395
|
// page, plus an 'Export Notes.txt'. `filters` carries the resolved values for
|
|
394
396
|
// every component together; each one picks out its own. `frequency` is the
|
|
395
|
-
// report's group-by grain (the server re-buckets only
|
|
396
|
-
// it), and `filterLines` is the filter bar as the viewer reads it
|
|
397
|
+
// report's group-by grain (the server re-buckets only the tiles that follow
|
|
398
|
+
// it: time-series charts and tables over a time axis), and `filterLines` is the filter bar as the viewer reads it
|
|
397
399
|
// (exportLines), which the notes file writes. Their text goes as
|
|
398
400
|
// filterSummary too, for a server that does not read filterLines; the
|
|
399
401
|
// lines are left out when there are none. `states` is each tile's own
|
|
@@ -2417,16 +2419,16 @@ const COMPARED_PRIOR_PREFIX = 'vs';
|
|
|
2417
2419
|
// The tooltip's last line on a chart whose marks act on a click
|
|
2418
2420
|
// (markActionHint): one action by its full name, several by their short
|
|
2419
2421
|
// names, in this order. Interface text, English in every report locale.
|
|
2420
|
-
const MARK_ACTION_ORDER = ['drill', 'crossFilter', '
|
|
2422
|
+
const MARK_ACTION_ORDER = ['drill', 'crossFilter', 'showRows'];
|
|
2421
2423
|
const MARK_ACTION_HINTS = {
|
|
2422
2424
|
drill: 'drill down',
|
|
2423
2425
|
crossFilter: 'filter other tiles',
|
|
2424
|
-
|
|
2426
|
+
showRows: 'show rows',
|
|
2425
2427
|
};
|
|
2426
2428
|
const MARK_ACTION_SHORT_HINTS = {
|
|
2427
2429
|
drill: 'drill down',
|
|
2428
2430
|
crossFilter: 'filter',
|
|
2429
|
-
|
|
2431
|
+
showRows: 'show rows',
|
|
2430
2432
|
};
|
|
2431
2433
|
// The words a statistic line's label starts with. They are interface text,
|
|
2432
2434
|
// English in every report locale; only the value after them follows it.
|
|
@@ -4460,6 +4462,19 @@ function crumbLabel(step, locale, full) {
|
|
|
4460
4462
|
return formatPeriodLabel(String(step.value), step.level, false, locale, full ? ReportChartDateLabel.Long : ReportChartDateLabel.Short);
|
|
4461
4463
|
}
|
|
4462
4464
|
|
|
4465
|
+
// Whether a component is a chart on a time x-axis.
|
|
4466
|
+
function isTimeSeriesChart(component) {
|
|
4467
|
+
return component.type === 'chart' && component.config?.xAxis?.kind === 'time';
|
|
4468
|
+
}
|
|
4469
|
+
// Whether the report's group-by grain applies to a tile: the server names the
|
|
4470
|
+
// field it re-buckets (followsGroupBy) on a time-series chart and on a table
|
|
4471
|
+
// over a time axis, and drops the grain for every other tile. A server that
|
|
4472
|
+
// predates tables following the group-by names none, and there a chart on a
|
|
4473
|
+
// time x-axis is the only tile that follows it.
|
|
4474
|
+
function followsGroupBy(component) {
|
|
4475
|
+
return !!component.followsGroupBy || isTimeSeriesChart(component);
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4463
4478
|
// A date-range boundary as the picked LOCAL calendar day (no time, no tz).
|
|
4464
4479
|
// Date-range filters are calendar-day semantics, so the boundary must carry
|
|
4465
4480
|
// only Y-M-D — never a timezone-bearing instant the backend would shift.
|
|
@@ -4796,11 +4811,12 @@ function listSearchItem(value) {
|
|
|
4796
4811
|
default: value ?? undefined,
|
|
4797
4812
|
};
|
|
4798
4813
|
}
|
|
4799
|
-
// Whether the report has a
|
|
4800
|
-
// x-axis
|
|
4801
|
-
//
|
|
4802
|
-
|
|
4803
|
-
|
|
4814
|
+
// Whether the report has a tile the group-by control can re-bucket: a chart
|
|
4815
|
+
// on a time x-axis or a table over a time axis (followsGroupBy). A time axis
|
|
4816
|
+
// is the whole qualification: the control changes its grain, and a category
|
|
4817
|
+
// axis has no grain to change.
|
|
4818
|
+
function hasGroupByTile(report) {
|
|
4819
|
+
return (report?.pages ?? []).some((page) => (page.components ?? []).some(followsGroupBy));
|
|
4804
4820
|
}
|
|
4805
4821
|
// The grains the report's group-by control offers, named for the viewer. WHICH
|
|
4806
4822
|
// of them comes from the report; how they read comes from GRANULARITY_OPTIONS.
|
|
@@ -4831,7 +4847,7 @@ function groupByFilterItem(report, initial) {
|
|
|
4831
4847
|
// Only an EXPLICIT false hides the control. An absent block means a report
|
|
4832
4848
|
// served by a build that predates the control being stored, and every one of
|
|
4833
4849
|
// those had one — the backend resolves it the same way (ReportGroupBy).
|
|
4834
|
-
if (groupBy?.enabled === false || !
|
|
4850
|
+
if (groupBy?.enabled === false || !hasGroupByTile(report)) {
|
|
4835
4851
|
return null;
|
|
4836
4852
|
}
|
|
4837
4853
|
const values = groupByOptions(report);
|
|
@@ -5141,6 +5157,10 @@ const MATCH_ANY_LINE = 'Filters in the report bar: match any';
|
|
|
5141
5157
|
const MATCH_ALL_CHIP = 'Match: all filters';
|
|
5142
5158
|
// A group's label when it has none.
|
|
5143
5159
|
const UNLABELED_GROUP = 'Filter';
|
|
5160
|
+
// How many picks a column filter's line names before 'and N more'.
|
|
5161
|
+
const GRID_FILTER_NAMED = 10;
|
|
5162
|
+
// What a blank key reads as in a column filter's line.
|
|
5163
|
+
const GRID_BLANK_KEY = '(blank)';
|
|
5144
5164
|
// The label of the group-by line when the report names no control.
|
|
5145
5165
|
const GROUP_BY_LABEL = 'Group By';
|
|
5146
5166
|
// One filter group's value as text ('Region: West, East'), or null when the
|
|
@@ -5359,6 +5379,68 @@ function pageName(report, pageId) {
|
|
|
5359
5379
|
}
|
|
5360
5380
|
return report.pages[index].name || `Page ${index + 1}`;
|
|
5361
5381
|
}
|
|
5382
|
+
// One column filter of a table as a file prints it: 'Region: East, West',
|
|
5383
|
+
// 'Exclude Region: North (rows with no Region are kept)', 'Customer: contains
|
|
5384
|
+
// "smith"', 'Amount: 10 to 50 (both included)', 'Ordered: Jan 2026 to Mar
|
|
5385
|
+
// 2026'. A key reads as its option's label, a blank key as (blank), known
|
|
5386
|
+
// from the null key itself; past GRID_FILTER_NAMED picks the rest are
|
|
5387
|
+
// counted. An exclude says what happens to the rows with no value: kept,
|
|
5388
|
+
// unless a blank key is among the keys left out. Null for a filter that
|
|
5389
|
+
// holds nothing.
|
|
5390
|
+
function gridFilterLineText(label, filter, field, locale) {
|
|
5391
|
+
const exclude = !!filter.exclude;
|
|
5392
|
+
const blanks = (excluded) => (exclude ? ` (rows with no ${label} ${excluded ? 'are left out' : 'are kept'})` : '');
|
|
5393
|
+
const line = (text, excluded = false) => `${exclude ? 'Exclude ' : ''}${label}: ${text}${blanks(excluded)}`;
|
|
5394
|
+
if (filter.keys?.length) {
|
|
5395
|
+
const names = filter.keys.slice(0, GRID_FILTER_NAMED).map((key) => gridKeyName(key, field));
|
|
5396
|
+
const more = filter.keys.length - names.length;
|
|
5397
|
+
return line(`${names.join(', ')}${more > 0 ? ` and ${more} more` : ''}`, filter.keys.some((key) => key === null));
|
|
5398
|
+
}
|
|
5399
|
+
const contains = (filter.contains ?? '').trim();
|
|
5400
|
+
if (contains) {
|
|
5401
|
+
return `${label}: ${exclude ? 'doesn\'t contain' : 'contains'} "${contains}"${blanks(false)}`;
|
|
5402
|
+
}
|
|
5403
|
+
const isDate = field?.type === GridFieldType.Date;
|
|
5404
|
+
const write = (end) => (isDate
|
|
5405
|
+
? gridDateText(String(end), filter.granularity ?? field?.granularities?.[0], locale)
|
|
5406
|
+
: formatPlainNumber(Number(end), locale));
|
|
5407
|
+
const has = (end) => end !== undefined && end !== null && end !== '';
|
|
5408
|
+
if (has(filter.from) && has(filter.to)) {
|
|
5409
|
+
return line(`${write(filter.from)} to ${write(filter.to)}${exclude || isDate ? '' : ' (both included)'}`);
|
|
5410
|
+
}
|
|
5411
|
+
if (has(filter.from)) {
|
|
5412
|
+
return line(`${write(filter.from)} ${isDate ? 'or later' : 'or more'}`);
|
|
5413
|
+
}
|
|
5414
|
+
return has(filter.to) ? line(`${write(filter.to)} ${isDate ? 'or earlier' : 'or less'}`) : null;
|
|
5415
|
+
}
|
|
5416
|
+
// A key as a column filter's line names it: its option's label, else itself.
|
|
5417
|
+
function gridKeyName(key, field) {
|
|
5418
|
+
if (key === null) {
|
|
5419
|
+
return GRID_BLANK_KEY;
|
|
5420
|
+
}
|
|
5421
|
+
return field?.options?.find((option) => option.key !== null && String(option.key) === String(key))?.label ?? String(key);
|
|
5422
|
+
}
|
|
5423
|
+
// A date end at the grain the filter reads: 'Jan 2026', 'Q1 2026', '2026',
|
|
5424
|
+
// else the day.
|
|
5425
|
+
function gridDateText(value, granularity, locale) {
|
|
5426
|
+
const match = /^(\d{4})-(\d{2})/.exec(value);
|
|
5427
|
+
if (!match) {
|
|
5428
|
+
return value;
|
|
5429
|
+
}
|
|
5430
|
+
const year = Number(match[1]);
|
|
5431
|
+
const month = Number(match[2]);
|
|
5432
|
+
switch (granularity) {
|
|
5433
|
+
case GridGranularity.Year:
|
|
5434
|
+
return String(year);
|
|
5435
|
+
case GridGranularity.Quarter:
|
|
5436
|
+
return `Q${Math.ceil(month / 3)} ${year}`;
|
|
5437
|
+
case GridGranularity.Month:
|
|
5438
|
+
return new Intl.DateTimeFormat(locale, { month: 'short', year: 'numeric', timeZone: 'UTC' })
|
|
5439
|
+
.format(new Date(Date.UTC(year, month - 1, 1)));
|
|
5440
|
+
default:
|
|
5441
|
+
return dateBoundLabel(value, locale) ?? value;
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5362
5444
|
|
|
5363
5445
|
// Filters inside text blocks: what each {{filter.<id>}} token reads as. The
|
|
5364
5446
|
// value is the viewer's current one for the group, the one the filter bar
|
|
@@ -5536,18 +5618,10 @@ const PAGE_DIMENSION_MAX = 50;
|
|
|
5536
5618
|
// ReportHeaderFooter::MAX_HEIGHT on the server, which clamps.
|
|
5537
5619
|
const HEADER_FOOTER_MAX = 3;
|
|
5538
5620
|
|
|
5539
|
-
// Whether a component is a chart on a time x-axis: the only component the
|
|
5540
|
-
// report's group-by grain applies to. A deliberate mirror of the server's
|
|
5541
|
-
// ReportQueryRunner::_isTimeSeriesChart, which is canonical and drops the
|
|
5542
|
-
// grain for every other component; kept so a product pinned to an older
|
|
5543
|
-
// framework does not re-bucket a non-time chart.
|
|
5544
|
-
function isTimeSeriesChart(component) {
|
|
5545
|
-
return component.type === 'chart' && component.config?.xAxis?.kind === 'time';
|
|
5546
|
-
}
|
|
5547
5621
|
// The interaction state one tile's data request carries, read off a snapshot
|
|
5548
|
-
// of the screen (the state service's snapshot()): the group-by grain
|
|
5549
|
-
//
|
|
5550
|
-
// chart's drill path, and, while the groups match 'any', the groups that
|
|
5622
|
+
// of the screen (the state service's snapshot()): the group-by grain for a
|
|
5623
|
+
// tile that follows it (followsGroupBy), a list's sort, search and hidden
|
|
5624
|
+
// columns, a chart's drill path, and, while the groups match 'any', the groups that
|
|
5551
5625
|
// combine (matchAnyGroupIds). The tile on screen, its CSV, the PDF and
|
|
5552
5626
|
// PowerPoint collector and the zip all build their state here, so a file
|
|
5553
5627
|
// always holds what the screen shows. A list's paging is the caller's. Never
|
|
@@ -5561,7 +5635,7 @@ function tileDataState(report, component, snapshot) {
|
|
|
5561
5635
|
return state;
|
|
5562
5636
|
}
|
|
5563
5637
|
const tile = snapshot.tiles?.[component.id];
|
|
5564
|
-
if (
|
|
5638
|
+
if (followsGroupBy(component) && isChartGranularity(snapshot.groupBy)) {
|
|
5565
5639
|
state.frequency = snapshot.groupBy;
|
|
5566
5640
|
}
|
|
5567
5641
|
if (component.type === 'list') {
|
|
@@ -5586,15 +5660,17 @@ function tileDataState(report, component, snapshot) {
|
|
|
5586
5660
|
}
|
|
5587
5661
|
// Each tile's state for the zip export (tileDataState), by component id: the
|
|
5588
5662
|
// tiles of every page, or of `pageId` alone. The zip holds no header or
|
|
5589
|
-
// footer tile. A
|
|
5590
|
-
//
|
|
5591
|
-
|
|
5663
|
+
// footer tile. A table adds the layout the viewer put it in (`gridLayout`,
|
|
5664
|
+
// null while it shows its stored one). A tile with nothing to send is left
|
|
5665
|
+
// out, so an untouched report sends no states at all.
|
|
5666
|
+
function zipExportStates(report, snapshot, pageId = null, gridLayout = () => null) {
|
|
5592
5667
|
const states = {};
|
|
5593
5668
|
(report.pages ?? [])
|
|
5594
5669
|
.filter((page) => !pageId || page.id === pageId)
|
|
5595
5670
|
.flatMap((page) => page.components ?? [])
|
|
5596
5671
|
.forEach((component) => {
|
|
5597
|
-
const
|
|
5672
|
+
const layout = component.type === 'grid' ? gridLayout(component) : null;
|
|
5673
|
+
const state = { ...tileDataState(report, component, snapshot), ...(layout ? { layout } : {}) };
|
|
5598
5674
|
if (Object.keys(state).length) {
|
|
5599
5675
|
states[component.id] = state;
|
|
5600
5676
|
}
|
|
@@ -5966,9 +6042,17 @@ function listPriorAlias(outputs, alias, output) {
|
|
|
5966
6042
|
: null;
|
|
5967
6043
|
}
|
|
5968
6044
|
|
|
5969
|
-
// A table tile's layouts: the one its config authors,
|
|
5970
|
-
//
|
|
6045
|
+
// A table tile's layouts: the one its config authors, the canonical key two
|
|
6046
|
+
// layouts are compared by, and a viewer's layout moved onto a new authored one.
|
|
5971
6047
|
const NO_LAYOUT = {};
|
|
6048
|
+
// The report's grains by the grid's name for each.
|
|
6049
|
+
const GRID_GRANULARITIES = {
|
|
6050
|
+
day: GridGranularity.Day,
|
|
6051
|
+
week: GridGranularity.Week,
|
|
6052
|
+
month: GridGranularity.Month,
|
|
6053
|
+
quarter: GridGranularity.Quarter,
|
|
6054
|
+
year: GridGranularity.Year,
|
|
6055
|
+
};
|
|
5972
6056
|
// The layout a table tile's config authors, or none.
|
|
5973
6057
|
function authoredGridLayout(component) {
|
|
5974
6058
|
return component.config?.layout ?? NO_LAYOUT;
|
|
@@ -5978,6 +6062,92 @@ function authoredGridLayout(component) {
|
|
|
5978
6062
|
function gridLayoutKey(layout) {
|
|
5979
6063
|
return JSON.stringify(canonicalGridLayout(layout));
|
|
5980
6064
|
}
|
|
6065
|
+
// A viewer's layout moved onto the layout authored now, `base` being the key
|
|
6066
|
+
// of the authored layout they started from: the new authored layout, with the
|
|
6067
|
+
// viewer's own filters and sort (what `base` did not have) kept on the fields
|
|
6068
|
+
// it still places. A field the viewer filters takes their filters in place of
|
|
6069
|
+
// the authored ones, and a sort they changed replaces the authored sort.
|
|
6070
|
+
function rebasedGridLayout(session, base, authored) {
|
|
6071
|
+
const started = JSON.parse(base);
|
|
6072
|
+
const viewer = canonicalGridLayout(session);
|
|
6073
|
+
const placed = new Set([
|
|
6074
|
+
...(authored.rows ?? []),
|
|
6075
|
+
...(authored.columns ?? []),
|
|
6076
|
+
...(authored.values ?? []),
|
|
6077
|
+
...(authored.details ?? []),
|
|
6078
|
+
].map((placement) => placement.field));
|
|
6079
|
+
const startedFilters = new Set((started.filters ?? []).map((filter) => JSON.stringify(filter)));
|
|
6080
|
+
const filters = (viewer.filters ?? [])
|
|
6081
|
+
.filter((filter) => placed.has(filter.field) && !startedFilters.has(JSON.stringify(filter)));
|
|
6082
|
+
const filtered = new Set(filters.map((filter) => filter.field));
|
|
6083
|
+
const sorted = JSON.stringify(viewer.sort ?? []) !== JSON.stringify(started.sort ?? []);
|
|
6084
|
+
const sort = (viewer.sort ?? []).filter((term) => placed.has(term.field));
|
|
6085
|
+
return canonicalGridLayout({
|
|
6086
|
+
...authored,
|
|
6087
|
+
filters: [...(authored.filters ?? []).filter((filter) => !filtered.has(filter.field)), ...filters],
|
|
6088
|
+
sort: sorted && sort.length ? sort : authored.sort,
|
|
6089
|
+
});
|
|
6090
|
+
}
|
|
6091
|
+
// A table's layout once the grain `field` reads at moves to `granularity` (the
|
|
6092
|
+
// report's group-by changed): its column filter on `field` converts to the
|
|
6093
|
+
// same days at the new grain when its edges line up, and is taken out
|
|
6094
|
+
// otherwise (convertDateFilter), so the read never sends a filter the field no
|
|
6095
|
+
// longer offers. `cleared` names the filter taken out and the grain the table
|
|
6096
|
+
// now groups by, as fs-grid's status line reads it. A layout with no filter on
|
|
6097
|
+
// the field, or one already at the grain, comes back as it is.
|
|
6098
|
+
function grainedGridLayout(layout, field, granularity) {
|
|
6099
|
+
const grain = GRID_GRANULARITIES[granularity];
|
|
6100
|
+
const filter = (layout.filters ?? []).find((item) => item.field === field);
|
|
6101
|
+
if (!filter || (filter.granularity ?? GridGranularity.Day) === grain) {
|
|
6102
|
+
return { layout, cleared: null };
|
|
6103
|
+
}
|
|
6104
|
+
const converted = convertDateFilter(filter, grain);
|
|
6105
|
+
const filters = (layout.filters ?? [])
|
|
6106
|
+
.map((item) => (item === filter ? converted : item))
|
|
6107
|
+
.filter((item) => !!item);
|
|
6108
|
+
return { layout: { ...layout, filters }, cleared: converted ? null : { field, granularity: grain } };
|
|
6109
|
+
}
|
|
6110
|
+
// The notes that leave a filter out of a read, by the filter's index (filters.N...)
|
|
6111
|
+
const PRUNED_FILTER_CODES = new Set([
|
|
6112
|
+
GridNoteCode.FieldMissing,
|
|
6113
|
+
GridNoteCode.CannotFilter,
|
|
6114
|
+
GridNoteCode.FilterRefused,
|
|
6115
|
+
GridNoteCode.GranularityRefused,
|
|
6116
|
+
]);
|
|
6117
|
+
// The notes that leave a sort term out of a read, by the term's index (sort.N...)
|
|
6118
|
+
const PRUNED_SORT_CODES = new Set([
|
|
6119
|
+
GridNoteCode.FieldMissing,
|
|
6120
|
+
GridNoteCode.SortRefused,
|
|
6121
|
+
]);
|
|
6122
|
+
// A viewer's layout of a table once its first read is in: the filters and sort
|
|
6123
|
+
// terms that read refused (a field the query no longer has, a filter or grain
|
|
6124
|
+
// the field no longer takes) are taken out, so the stored session layout holds
|
|
6125
|
+
// only what applies. When the author turned filtering off (`filter`, the
|
|
6126
|
+
// tile's controls.filter), the viewer's filters give way to the authored ones
|
|
6127
|
+
// (`authored`), which a viewer cannot change. The layout comes back as it is
|
|
6128
|
+
// when nothing changes.
|
|
6129
|
+
function prunedGridLayout(layout, notes, filter, authored = []) {
|
|
6130
|
+
const filters = layout.filters ?? [];
|
|
6131
|
+
const sort = layout.sort ?? [];
|
|
6132
|
+
const filterIndexes = notedIndexes(notes, 'filters', PRUNED_FILTER_CODES);
|
|
6133
|
+
const sortIndexes = notedIndexes(notes, 'sort', PRUNED_SORT_CODES);
|
|
6134
|
+
const canFilter = (filter ?? GridControlState.Enabled) === GridControlState.Enabled;
|
|
6135
|
+
const keptFilters = canFilter ? filters.filter((_item, index) => !filterIndexes.has(index)) : [...authored];
|
|
6136
|
+
const keptSort = sort.filter((_term, index) => !sortIndexes.has(index));
|
|
6137
|
+
const isFiltersKept = JSON.stringify(keptFilters) === JSON.stringify(filters);
|
|
6138
|
+
return isFiltersKept && keptSort.length === sort.length
|
|
6139
|
+
? layout
|
|
6140
|
+
: { ...layout, filters: keptFilters, sort: keptSort };
|
|
6141
|
+
}
|
|
6142
|
+
// The indexes under `key` (key.N or key.N.part) the notes of `codes` name
|
|
6143
|
+
function notedIndexes(notes, key, codes) {
|
|
6144
|
+
const pattern = new RegExp(`^${key}\\.(\\d+)(\\.|$)`);
|
|
6145
|
+
return new Set(notes
|
|
6146
|
+
.filter((note) => codes.has(note.code))
|
|
6147
|
+
.map((note) => pattern.exec(note.path ?? ''))
|
|
6148
|
+
.filter((match) => !!match)
|
|
6149
|
+
.map((match) => Number(match[1])));
|
|
6150
|
+
}
|
|
5981
6151
|
|
|
5982
6152
|
// Whether two stored filter-group defaults are the same. Compared on the fields a
|
|
5983
6153
|
// default can actually carry, so key order in the payload can't read as a change.
|
|
@@ -6070,7 +6240,7 @@ function defaultSignature(group) {
|
|
|
6070
6240
|
}
|
|
6071
6241
|
// The temporary layer with nothing picked.
|
|
6072
6242
|
function emptyTemporaryState() {
|
|
6073
|
-
return { crossFilters: [],
|
|
6243
|
+
return { crossFilters: [], showRows: null };
|
|
6074
6244
|
}
|
|
6075
6245
|
// Viewer filter state for the open report. Filter VALUES are never saved to
|
|
6076
6246
|
// the report — this service holds them per filter GROUP (the propagation unit),
|
|
@@ -6140,9 +6310,11 @@ class ReportFilterStateService {
|
|
|
6140
6310
|
// Whether the screen differs from the report's authored defaults
|
|
6141
6311
|
// (stateDiffers): what the report bar's 'Clear filters' shows for. A switch
|
|
6142
6312
|
// of page does not count, and the temporary state never does.
|
|
6313
|
+
// A table tile in a layout of the viewer's own counts too; it stays out of
|
|
6314
|
+
// the snapshot, so no saved filter or link carries it.
|
|
6143
6315
|
differsFromDefaults = computed(() => {
|
|
6144
6316
|
this._revision();
|
|
6145
|
-
return stateDiffers(this.authoredConfig(), this.snapshot());
|
|
6317
|
+
return this._gridLayouts().size > 0 || stateDiffers(this.authoredConfig(), this.snapshot());
|
|
6146
6318
|
});
|
|
6147
6319
|
_values = new Map();
|
|
6148
6320
|
_groups = new Map();
|
|
@@ -6152,7 +6324,8 @@ class ReportFilterStateService {
|
|
|
6152
6324
|
// its default straight back. A change to the stored default releases it.
|
|
6153
6325
|
_cleared = new Set();
|
|
6154
6326
|
// The report's group-by control: a single report-wide bucket override for all
|
|
6155
|
-
//
|
|
6327
|
+
// the tiles that follow it (followsGroupBy). Unset = each one uses its
|
|
6328
|
+
// authored granularity. Never
|
|
6156
6329
|
// saved to the report, and carried in the data request's
|
|
6157
6330
|
// `state`, so it propagates through to the PDF/PowerPoint exports too. Named
|
|
6158
6331
|
// for the `frequency` wire key it travels under, not for the control's label.
|
|
@@ -6212,6 +6385,16 @@ class ReportFilterStateService {
|
|
|
6212
6385
|
// changed from (ReportGridSessionLayout.base). A table showing its authored
|
|
6213
6386
|
// layout has no entry, and only a table has one.
|
|
6214
6387
|
_gridLayouts = signal(new Map());
|
|
6388
|
+
// The groups a viewer left open in each table tile, by component id, so a
|
|
6389
|
+
// tile drawn again (a page switch) opens them. Session only: no Clear
|
|
6390
|
+
// resets them, differsFromDefaults never reads them, and fs-grid drops
|
|
6391
|
+
// them itself once the row levels change.
|
|
6392
|
+
_gridExpansions = signal(new Map());
|
|
6393
|
+
// By table tile, the column filter a group-by change took out of it and
|
|
6394
|
+
// the grain the table now groups by (_grainGridLayouts()), for fs-grid's
|
|
6395
|
+
// status line. fs-grid hides it once the field is filtered again; a Clear
|
|
6396
|
+
// drops it.
|
|
6397
|
+
_gridClearedFilters = signal(new Map());
|
|
6215
6398
|
// The temporary layer: kept apart from everything above, so snapshot()
|
|
6216
6399
|
// and differsFromDefaults never read it.
|
|
6217
6400
|
_temporary = signal(emptyTemporaryState());
|
|
@@ -6278,14 +6461,18 @@ class ReportFilterStateService {
|
|
|
6278
6461
|
frequency() {
|
|
6279
6462
|
return this._frequency;
|
|
6280
6463
|
}
|
|
6464
|
+
// Each table that follows the group-by has its filter on the group-by's
|
|
6465
|
+
// field moved to the new grain first (_grainGridLayouts()), so a table on
|
|
6466
|
+
// screen reads once, with both.
|
|
6281
6467
|
setFrequency(frequency) {
|
|
6282
6468
|
this._frequency = frequency;
|
|
6469
|
+
this._grainGridLayouts();
|
|
6283
6470
|
this._frequencyChanged$.next();
|
|
6284
6471
|
this._stateChanged$.next();
|
|
6285
6472
|
}
|
|
6286
|
-
// Fires whenever the group-by grain changes — every
|
|
6287
|
-
// so it refetches and re-buckets (the override isn't scoped to any
|
|
6288
|
-
// group, so changesFor() can't carry it).
|
|
6473
|
+
// Fires whenever the group-by grain changes — every tile that follows it
|
|
6474
|
+
// listens so it refetches and re-buckets (the override isn't scoped to any
|
|
6475
|
+
// filter group, so changesFor() can't carry it).
|
|
6289
6476
|
frequencyChanges() {
|
|
6290
6477
|
return this._frequencyChanged$.asObservable();
|
|
6291
6478
|
}
|
|
@@ -6538,24 +6725,27 @@ class ReportFilterStateService {
|
|
|
6538
6725
|
// the tile reads as stored again. fs-grid reads the change itself, so only
|
|
6539
6726
|
// changes() hears about it, and only on a real change.
|
|
6540
6727
|
setGridLayout(component, layout) {
|
|
6541
|
-
|
|
6542
|
-
|
|
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;
|
|
6728
|
+
if (this._putGridLayout(component, layout)) {
|
|
6729
|
+
this._stateChanged$.next();
|
|
6549
6730
|
}
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6731
|
+
}
|
|
6732
|
+
// The column filter a group-by change took out of a table tile, or null
|
|
6733
|
+
// (_gridClearedFilters).
|
|
6734
|
+
gridClearedFilter(component) {
|
|
6735
|
+
return this._gridClearedFilters().get(component.id) ?? null;
|
|
6736
|
+
}
|
|
6737
|
+
// The groups a viewer left open in a table tile, or undefined: the tile
|
|
6738
|
+
// opens as its layout says.
|
|
6739
|
+
gridExpansion(component) {
|
|
6740
|
+
return this._gridExpansions().get(component.id);
|
|
6741
|
+
}
|
|
6742
|
+
// Records the groups open in a table tile, as fs-grid writes them out. It
|
|
6743
|
+
// changes no read, so nothing hears about it.
|
|
6744
|
+
setGridExpansion(component, expansion) {
|
|
6745
|
+
if (component.type !== 'grid' || !expansion || this._gridExpansions().get(component.id) === expansion) {
|
|
6746
|
+
return;
|
|
6556
6747
|
}
|
|
6557
|
-
this.
|
|
6558
|
-
this._stateChanged$.next();
|
|
6748
|
+
this._gridExpansions.set(new Map(this._gridExpansions()).set(component.id, expansion));
|
|
6559
6749
|
}
|
|
6560
6750
|
// The durable state on screen as one document: the page, every group the
|
|
6561
6751
|
// report has (null when it holds nothing, a cleared default included), the
|
|
@@ -6921,6 +7111,56 @@ class ReportFilterStateService {
|
|
|
6921
7111
|
this.listHidden.set(new Map());
|
|
6922
7112
|
this._drills.set(new Map());
|
|
6923
7113
|
this._gridLayouts.set(new Map());
|
|
7114
|
+
this._gridClearedFilters.set(new Map());
|
|
7115
|
+
}
|
|
7116
|
+
// setGridLayout() without the change event; true when the layout changed.
|
|
7117
|
+
_putGridLayout(component, layout) {
|
|
7118
|
+
const base = gridLayoutKey(authoredGridLayout(component));
|
|
7119
|
+
const key = gridLayoutKey(layout);
|
|
7120
|
+
const current = this._gridLayouts().get(component.id);
|
|
7121
|
+
const unchanged = key === base
|
|
7122
|
+
? !current
|
|
7123
|
+
: current?.base === base && gridLayoutKey(current.layout) === key;
|
|
7124
|
+
if (component.type !== 'grid' || unchanged) {
|
|
7125
|
+
return false;
|
|
7126
|
+
}
|
|
7127
|
+
const next = new Map(this._gridLayouts());
|
|
7128
|
+
if (key === base) {
|
|
7129
|
+
next.delete(component.id);
|
|
7130
|
+
}
|
|
7131
|
+
else {
|
|
7132
|
+
next.set(component.id, { layout, base });
|
|
7133
|
+
}
|
|
7134
|
+
this._gridLayouts.set(next);
|
|
7135
|
+
return true;
|
|
7136
|
+
}
|
|
7137
|
+
// Every table that follows the group-by (followsGroupBy), its column filter
|
|
7138
|
+
// on the group-by's field moved to the grain it now reads at: the viewer's
|
|
7139
|
+
// pick, else the table's own (grainedGridLayout()). Tables on other pages
|
|
7140
|
+
// move too, so their files, the chat and their next paint read the moved
|
|
7141
|
+
// filter. A filter taken out is noted on its tile (gridClearedFilter()); a
|
|
7142
|
+
// note already there names the new grain, unless the field is filtered
|
|
7143
|
+
// again, which ends it.
|
|
7144
|
+
_grainGridLayouts() {
|
|
7145
|
+
const cleared = new Map(this._gridClearedFilters());
|
|
7146
|
+
this._tiles().forEach((component) => {
|
|
7147
|
+
const groupBy = component.type === 'grid' ? component.followsGroupBy : null;
|
|
7148
|
+
const granularity = this._frequency ?? groupBy?.granularity;
|
|
7149
|
+
if (!groupBy || !granularity) {
|
|
7150
|
+
return;
|
|
7151
|
+
}
|
|
7152
|
+
const grained = grainedGridLayout(this.gridLayout(component), groupBy.field, granularity);
|
|
7153
|
+
const filtered = (grained.layout.filters ?? []).some((filter) => filter.field === groupBy.field);
|
|
7154
|
+
const note = grained.cleared ?? (filtered ? null : cleared.get(component.id));
|
|
7155
|
+
if (note) {
|
|
7156
|
+
cleared.set(component.id, { ...note, granularity: GRID_GRANULARITIES[granularity] });
|
|
7157
|
+
}
|
|
7158
|
+
else {
|
|
7159
|
+
cleared.delete(component.id);
|
|
7160
|
+
}
|
|
7161
|
+
this._putGridLayout(component, grained.layout);
|
|
7162
|
+
});
|
|
7163
|
+
this._gridClearedFilters.set(cleared);
|
|
6924
7164
|
}
|
|
6925
7165
|
// One chart's drill path; an empty path is the chart's top.
|
|
6926
7166
|
_setDrillPath(componentId, path) {
|
|
@@ -6933,8 +7173,8 @@ class ReportFilterStateService {
|
|
|
6933
7173
|
}
|
|
6934
7174
|
this._drills.set(next);
|
|
6935
7175
|
}
|
|
6936
|
-
// After resetToDefaults(): every bar and tile repaints, and the
|
|
6937
|
-
//
|
|
7176
|
+
// After resetToDefaults(): every bar and tile repaints, and the tiles that
|
|
7177
|
+
// follow the group-by refetch when the grain moved.
|
|
6938
7178
|
_notifyReset(frequency) {
|
|
6939
7179
|
this._stateVersion.update((version) => version + 1);
|
|
6940
7180
|
this._changed$.next(null);
|
|
@@ -6954,39 +7194,55 @@ class ReportFilterStateService {
|
|
|
6954
7194
|
return this._tiles()
|
|
6955
7195
|
.some((component) => component.id === componentId && component.type === 'chart');
|
|
6956
7196
|
}
|
|
6957
|
-
// Drops each table layout whose tile is gone
|
|
6958
|
-
// authored otherwise
|
|
7197
|
+
// Drops each table layout and expansion whose tile is gone or is no table
|
|
7198
|
+
// any more. A layout whose tile is now authored otherwise (an edit, the
|
|
7199
|
+
// agent's) moves onto the new authored layout, keeping the viewer's filters
|
|
7200
|
+
// and sort terms on fields it still places (rebasedGridLayout), so the tile
|
|
7201
|
+
// shows both changes; one that then draws the authored layout is dropped.
|
|
6959
7202
|
_pruneGridLayouts() {
|
|
6960
7203
|
const grids = new Map(this._tiles()
|
|
6961
7204
|
.filter((component) => component.type === 'grid')
|
|
6962
7205
|
.map((component) => [component.id, component]));
|
|
6963
|
-
const kept = new Map(
|
|
6964
|
-
|
|
7206
|
+
const kept = new Map();
|
|
7207
|
+
this._gridLayouts().forEach((session, componentId) => {
|
|
6965
7208
|
const component = grids.get(componentId);
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
7209
|
+
if (!component) {
|
|
7210
|
+
return;
|
|
7211
|
+
}
|
|
7212
|
+
const authored = authoredGridLayout(component);
|
|
7213
|
+
const base = gridLayoutKey(authored);
|
|
7214
|
+
const layout = session.base === base ? session.layout : rebasedGridLayout(session.layout, session.base, authored);
|
|
7215
|
+
if (gridLayoutKey(layout) !== base) {
|
|
7216
|
+
kept.set(componentId, { layout, base });
|
|
7217
|
+
}
|
|
7218
|
+
});
|
|
7219
|
+
this._gridLayouts.set(kept);
|
|
7220
|
+
this._gridExpansions.set(new Map([...this._gridExpansions()]
|
|
7221
|
+
.filter(([componentId]) => grids.has(componentId))));
|
|
7222
|
+
this._gridClearedFilters.set(new Map([...this._gridClearedFilters()]
|
|
7223
|
+
.filter(([componentId]) => grids.has(componentId))));
|
|
6971
7224
|
}
|
|
6972
7225
|
// Keeps the page, drill and table state to what the report (re)loaded by
|
|
6973
7226
|
// 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
|
|
6975
|
-
//
|
|
6976
|
-
//
|
|
6977
|
-
// 'all', no drill, every table as authored and nothing temporary.
|
|
7227
|
+
// has it, and each table's layout and open groups while it is still a table
|
|
7228
|
+
// (_pruneGridLayouts). On another report it starts again: match 'all', no
|
|
7229
|
+
// drill, every table as authored and closed, and nothing temporary.
|
|
6978
7230
|
_prunePageState(report) {
|
|
6979
7231
|
if (this._report()?.id !== report.id) {
|
|
6980
7232
|
this._match.set('all');
|
|
6981
7233
|
this._pageId.set(null);
|
|
6982
7234
|
this._drills.set(new Map());
|
|
6983
7235
|
this._gridLayouts.set(new Map());
|
|
7236
|
+
this._gridExpansions.set(new Map());
|
|
7237
|
+
this._gridClearedFilters.set(new Map());
|
|
6984
7238
|
this._temporary.set(emptyTemporaryState());
|
|
6985
7239
|
}
|
|
6986
7240
|
this._report.set(report);
|
|
6987
7241
|
this._drills.set(new Map([...this._drills()]
|
|
6988
7242
|
.filter(([componentId]) => this._isChart(componentId))));
|
|
6989
7243
|
this._pruneGridLayouts();
|
|
7244
|
+
// A table the report now authors otherwise reads at the grain on screen.
|
|
7245
|
+
this._grainGridLayouts();
|
|
6990
7246
|
if (this._pageId() && !(report.pages ?? []).some((page) => page.id === this._pageId())) {
|
|
6991
7247
|
this._pageId.set(null);
|
|
6992
7248
|
}
|
|
@@ -7579,7 +7835,7 @@ function normalizedCardSettings(settings) {
|
|
|
7579
7835
|
const MARK_ACTION_LABELS = {
|
|
7580
7836
|
drill: 'Drill down',
|
|
7581
7837
|
crossFilter: 'Filter other tiles',
|
|
7582
|
-
|
|
7838
|
+
showRows: 'Show rows',
|
|
7583
7839
|
};
|
|
7584
7840
|
// Where a click was and what made it. A PointerEvent names its pointer; a
|
|
7585
7841
|
// TouchEvent is a touch; anything else (a plain MouseEvent, a key press
|
|
@@ -8181,6 +8437,9 @@ class ReportGridSource {
|
|
|
8181
8437
|
_componentId;
|
|
8182
8438
|
// The report line of the latest read, null until one arrives.
|
|
8183
8439
|
report = signal(null);
|
|
8440
|
+
// The summary of the latest read (its version and notes), null until one
|
|
8441
|
+
// arrives.
|
|
8442
|
+
summary = signal(null);
|
|
8184
8443
|
_isExport;
|
|
8185
8444
|
_locale;
|
|
8186
8445
|
constructor(_reportData, _reportId, _componentId, options = {}) {
|
|
@@ -8200,7 +8459,13 @@ class ReportGridSource {
|
|
|
8200
8459
|
return EMPTY;
|
|
8201
8460
|
}
|
|
8202
8461
|
return this._part$(event.data);
|
|
8203
|
-
}), concatWith(defer(() =>
|
|
8462
|
+
}), concatWith(defer(() => {
|
|
8463
|
+
if (!summary) {
|
|
8464
|
+
return throwError(() => new Error(STREAM_CUT));
|
|
8465
|
+
}
|
|
8466
|
+
this.summary.set(summary);
|
|
8467
|
+
return of(summary);
|
|
8468
|
+
})), catchError$1((error) => throwError(() => readError(error))));
|
|
8204
8469
|
});
|
|
8205
8470
|
}
|
|
8206
8471
|
_stream$(request, params) {
|
|
@@ -8255,6 +8520,44 @@ function readError(error) {
|
|
|
8255
8520
|
return error instanceof Error ? error : new Error(READ_FAILED);
|
|
8256
8521
|
}
|
|
8257
8522
|
|
|
8523
|
+
// A detail cell of an output field (a prior, a change, a % change, a rank)
|
|
8524
|
+
// as a list writes it: a change reads its prior beside it on the line, so a
|
|
8525
|
+
// missing baseline reads '—' and a zero one 'n/a', and the cell's title says
|
|
8526
|
+
// why, as the list's hover does. A value it holds keeps the grid's own text
|
|
8527
|
+
// (its placement's format); a rank change is written whole, since a new rank
|
|
8528
|
+
// reads apart from an unranked line. Every other cell is the grid's text,
|
|
8529
|
+
// with no title.
|
|
8530
|
+
class ReportGridCellPipe {
|
|
8531
|
+
// The cell's raw value, its field, its line's record (absent on a group or
|
|
8532
|
+
// total row) and the grid's own text, from the fsGridCell context.
|
|
8533
|
+
transform(value, field, record, text, report, format) {
|
|
8534
|
+
const name = field?.name ?? '';
|
|
8535
|
+
const output = record ? report?.outputs?.[name] : null;
|
|
8536
|
+
if (!output || !record) {
|
|
8537
|
+
return { text, title: null };
|
|
8538
|
+
}
|
|
8539
|
+
const priorAlias = output.role === 'change' || output.role === 'percent_change'
|
|
8540
|
+
? laneOutputAlias(report?.outputs, name, 'prior')
|
|
8541
|
+
: null;
|
|
8542
|
+
const formatted = formatOutputValue(value, output, null, format, {
|
|
8543
|
+
...(priorAlias ? { prior: record[priorAlias] } : {}),
|
|
8544
|
+
floor: report?.comparison?.floor ?? null,
|
|
8545
|
+
empty: '',
|
|
8546
|
+
});
|
|
8547
|
+
const isMissing = value === null || value === undefined || value === '';
|
|
8548
|
+
return { text: isMissing || output.kind === 'rank' ? formatted.text : text, title: formatted.title };
|
|
8549
|
+
}
|
|
8550
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportGridCellPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
8551
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.14", ngImport: i0, type: ReportGridCellPipe, isStandalone: true, name: "fsReportGridCell" });
|
|
8552
|
+
}
|
|
8553
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportGridCellPipe, decorators: [{
|
|
8554
|
+
type: Pipe,
|
|
8555
|
+
args: [{
|
|
8556
|
+
name: 'fsReportGridCell',
|
|
8557
|
+
standalone: true,
|
|
8558
|
+
}]
|
|
8559
|
+
}] });
|
|
8560
|
+
|
|
8258
8561
|
const VALUE_FORMATS = {
|
|
8259
8562
|
[GridFormat.Number]: ReportValueFormat.Number,
|
|
8260
8563
|
[GridFormat.Percent]: ReportValueFormat.Percent,
|
|
@@ -8354,12 +8657,24 @@ function decimalMark(locale) {
|
|
|
8354
8657
|
// under an older state version is dropped.
|
|
8355
8658
|
// - The layout on screen is the state service's (gridLayout): the viewer's
|
|
8356
8659
|
// 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.
|
|
8660
|
+
// authored one. It outlives a page switch, and the chat reads it. So do
|
|
8661
|
+
// the groups the viewer left open (gridExpansion), which a Clear leaves.
|
|
8358
8662
|
// - A state version move (Clear filters, a saved filter) skips the wait; the
|
|
8359
8663
|
// reset has put the authored layout back in the same tick, so it costs one
|
|
8360
8664
|
// read.
|
|
8665
|
+
// - A table that follows the report's group-by (followsGroupBy) sends its
|
|
8666
|
+
// grain, and reads again at once when it changes. The state service has
|
|
8667
|
+
// moved its filter on the group-by's field to the new grain by then, or
|
|
8668
|
+
// taken it out (clearedFilter, which the status line names), so one read
|
|
8669
|
+
// goes out and nothing paints unfiltered.
|
|
8361
8670
|
// - fs-grid compares the layout and params by value: a cloned value never
|
|
8362
8671
|
// reads again.
|
|
8672
|
+
// - An output's detail cell is drawn by fsReportGridCell, so a change tells
|
|
8673
|
+
// a missing prior from a zero one, as a list does; a click on it opens
|
|
8674
|
+
// why, as no cell carries a native tooltip.
|
|
8675
|
+
// - After the tile's first read, the viewer's layout loses the filters and
|
|
8676
|
+
// sort terms that read refused (prunedGridLayout), so stale tile state
|
|
8677
|
+
// (a field the AI took out of the query) does not linger.
|
|
8363
8678
|
class ComponentGridComponent {
|
|
8364
8679
|
reportId = input.required();
|
|
8365
8680
|
component = input.required();
|
|
@@ -8369,7 +8684,11 @@ class ComponentGridComponent {
|
|
|
8369
8684
|
locale: () => this._filterState.formatContext().locale,
|
|
8370
8685
|
}));
|
|
8371
8686
|
layout = computed(() => this._filterState.gridLayout(this.component()));
|
|
8687
|
+
clearedFilter = computed(() => this._filterState.gridClearedFilter(this.component()));
|
|
8688
|
+
// Read once: fs-grid owns the open groups from then on and writes them out.
|
|
8689
|
+
expansion = signal(undefined);
|
|
8372
8690
|
noteData = computed(() => this.source().report());
|
|
8691
|
+
formatContext = computed(() => this._filterState.formatContext());
|
|
8373
8692
|
formatter = computed(() => reportGridFormatter({
|
|
8374
8693
|
format: this._filterState.formatContext(),
|
|
8375
8694
|
timezone: this._filterState.timezone(),
|
|
@@ -8379,20 +8698,57 @@ class ComponentGridComponent {
|
|
|
8379
8698
|
_reportData = inject(ReportData);
|
|
8380
8699
|
_filterState = inject(ReportFilterStateService);
|
|
8381
8700
|
_destroyRef = inject(DestroyRef);
|
|
8701
|
+
_injector = inject(Injector);
|
|
8702
|
+
// Whether the first read's notes have pruned the viewer's layout.
|
|
8703
|
+
_isPruned = false;
|
|
8382
8704
|
// The state version the params on screen were read under.
|
|
8383
8705
|
_paramsVersion = 0;
|
|
8384
8706
|
ngOnInit() {
|
|
8707
|
+
this.expansion.set(this._filterState.gridExpansion(this.component()));
|
|
8385
8708
|
this._init();
|
|
8709
|
+
this._pruneAfterFirstRead();
|
|
8386
8710
|
}
|
|
8387
8711
|
setLayout(layout) {
|
|
8388
8712
|
this._filterState.setGridLayout(this.component(), layout);
|
|
8389
8713
|
}
|
|
8714
|
+
setExpansion(expansion) {
|
|
8715
|
+
this._filterState.setGridExpansion(this.component(), expansion);
|
|
8716
|
+
}
|
|
8390
8717
|
_init() {
|
|
8391
8718
|
this._paramsVersion = this._filterState.stateVersion();
|
|
8392
8719
|
this.params.set(this._readParams());
|
|
8393
8720
|
this._filterState.changesFor(this.component())
|
|
8394
8721
|
.pipe(switchMap$1(() => this._settled$()), tap$1(() => this._applyParams()), takeUntilDestroyed(this._destroyRef))
|
|
8395
8722
|
.subscribe();
|
|
8723
|
+
if (followsGroupBy(this.component())) {
|
|
8724
|
+
this._filterState.frequencyChanges()
|
|
8725
|
+
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
8726
|
+
.subscribe(() => this._applyParams());
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
// Once the tile's first read is in, its notes prune the viewer's layout
|
|
8730
|
+
// (prunedGridLayout). A tile showing its authored layout has nothing to
|
|
8731
|
+
// prune.
|
|
8732
|
+
_pruneAfterFirstRead() {
|
|
8733
|
+
effect(() => {
|
|
8734
|
+
const summary = this.source().summary();
|
|
8735
|
+
if (!summary || this._isPruned) {
|
|
8736
|
+
return;
|
|
8737
|
+
}
|
|
8738
|
+
this._isPruned = true;
|
|
8739
|
+
untracked(() => this._prune(summary.notes ?? []));
|
|
8740
|
+
}, { injector: this._injector, allowSignalWrites: true });
|
|
8741
|
+
}
|
|
8742
|
+
_prune(notes) {
|
|
8743
|
+
const component = this.component();
|
|
8744
|
+
const session = this._filterState.gridSessionLayout(component);
|
|
8745
|
+
if (!session) {
|
|
8746
|
+
return;
|
|
8747
|
+
}
|
|
8748
|
+
const pruned = prunedGridLayout(session, notes, this.controls().filter, authoredGridLayout(component).filters);
|
|
8749
|
+
if (pruned !== session) {
|
|
8750
|
+
this._filterState.setGridLayout(component, pruned);
|
|
8751
|
+
}
|
|
8396
8752
|
}
|
|
8397
8753
|
// A change, once it has settled: at once when the state version moved,
|
|
8398
8754
|
// else after the debounce; dropped when the version moves while it waits,
|
|
@@ -8407,24 +8763,29 @@ class ComponentGridComponent {
|
|
|
8407
8763
|
this._paramsVersion = this._filterState.stateVersion();
|
|
8408
8764
|
this.params.set(this._readParams());
|
|
8409
8765
|
}
|
|
8410
|
-
// The tile's report filters
|
|
8766
|
+
// The tile's report filters, 'any' groups and group-by grain as they stand
|
|
8767
|
+
// (tileState()).
|
|
8411
8768
|
_readParams() {
|
|
8412
8769
|
const component = this.component();
|
|
8413
|
-
const matchAnyGroupIds = this._filterState.tileState(component)
|
|
8770
|
+
const { matchAnyGroupIds, frequency } = this._filterState.tileState(component);
|
|
8414
8771
|
return {
|
|
8415
8772
|
filters: this._filterState.resolveForComponent(component),
|
|
8416
8773
|
...(matchAnyGroupIds?.length ? { matchAnyGroupIds } : {}),
|
|
8774
|
+
...(frequency ? { frequency } : {}),
|
|
8417
8775
|
};
|
|
8418
8776
|
}
|
|
8419
8777
|
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 });
|
|
8778
|
+
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 [expansion]=\"expansion()\"\n [clearedFilter]=\"clearedFilter()\"\n (expansionChange)=\"setExpansion($event)\"\n [params]=\"params\"\n [controls]=\"controls()\"\n [formatter]=\"formatter()\"\n (layoutChange)=\"setLayout($event)\">\n <ng-template fsGridCell let-value let-field=\"field\" let-record=\"record\" let-text=\"text\">\n @if (value | fsReportGridCell: field : record : text : noteData() : formatContext(); as shown) {\n <!-- A cell that says why (a missing or zero prior) opens it on a\n click; no cell carries a native tooltip. -->\n @if (shown.title) {\n <span\n class=\"explained\"\n fsPopover\n trigger=\"click\"\n [template]=\"explanation\"\n [indication]=\"false\"\n [maxWidth]=\"320\">\n {{ shown.text }}\n </span>\n <ng-template #explanation>{{ shown.title }}</ng-template>\n } @else {\n <span>{{ shown.text }}</span>\n }\n }\n </ng-template>\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}.explained{cursor:pointer;text-decoration:underline dotted;text-underline-offset:2px}\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: "directive", type: FsGridCellDirective, selector: "[fsGridCell]" }, { 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"] }, { kind: "component", type: ComponentNoteComponent, selector: "app-report-component-note", inputs: ["data", "windows", "windowDescriptions", "windowNotes"] }, { kind: "pipe", type: ReportGridCellPipe, name: "fsReportGridCell" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8421
8779
|
}
|
|
8422
8780
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ComponentGridComponent, decorators: [{
|
|
8423
8781
|
type: Component,
|
|
8424
8782
|
args: [{ selector: 'app-report-component-grid', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
|
|
8425
8783
|
FsGridComponent,
|
|
8784
|
+
FsGridCellDirective,
|
|
8785
|
+
FsPopoverDirective,
|
|
8426
8786
|
ComponentNoteComponent,
|
|
8427
|
-
|
|
8787
|
+
ReportGridCellPipe,
|
|
8788
|
+
], 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 [expansion]=\"expansion()\"\n [clearedFilter]=\"clearedFilter()\"\n (expansionChange)=\"setExpansion($event)\"\n [params]=\"params\"\n [controls]=\"controls()\"\n [formatter]=\"formatter()\"\n (layoutChange)=\"setLayout($event)\">\n <ng-template fsGridCell let-value let-field=\"field\" let-record=\"record\" let-text=\"text\">\n @if (value | fsReportGridCell: field : record : text : noteData() : formatContext(); as shown) {\n <!-- A cell that says why (a missing or zero prior) opens it on a\n click; no cell carries a native tooltip. -->\n @if (shown.title) {\n <span\n class=\"explained\"\n fsPopover\n trigger=\"click\"\n [template]=\"explanation\"\n [indication]=\"false\"\n [maxWidth]=\"320\">\n {{ shown.text }}\n </span>\n <ng-template #explanation>{{ shown.title }}</ng-template>\n } @else {\n <span>{{ shown.text }}</span>\n }\n }\n </ng-template>\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}.explained{cursor:pointer;text-decoration:underline dotted;text-underline-offset:2px}\n"] }]
|
|
8428
8789
|
}] });
|
|
8429
8790
|
|
|
8430
8791
|
// A KPI's trend over its last periods: a line that stretches to the card's
|
|
@@ -10135,9 +10496,9 @@ class ReportComponentComponent {
|
|
|
10135
10496
|
this._filterState.changesFor(this.component)
|
|
10136
10497
|
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
10137
10498
|
.subscribe(() => this._refresh$.next());
|
|
10138
|
-
// A group-by change re-buckets every
|
|
10139
|
-
// to a filter group, so it rides its own stream.
|
|
10140
|
-
if (this.
|
|
10499
|
+
// A group-by change re-buckets every tile that follows it; it isn't
|
|
10500
|
+
// scoped to a filter group, so it rides its own stream.
|
|
10501
|
+
if (followsGroupBy(this.component)) {
|
|
10141
10502
|
this._filterState.frequencyChanges()
|
|
10142
10503
|
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
10143
10504
|
.subscribe(() => {
|
|
@@ -10327,17 +10688,18 @@ class ReportComponentComponent {
|
|
|
10327
10688
|
}
|
|
10328
10689
|
}
|
|
10329
10690
|
// The CSV is what the screen shows, so it carries the tile's interaction
|
|
10330
|
-
// state (tileDataState): the group-by grain for a
|
|
10691
|
+
// state (tileDataState): the group-by grain for a tile that follows it, a
|
|
10331
10692
|
// chart's drill path, the viewer's header sort, search and hidden columns
|
|
10332
10693
|
// for a list (a searched list exports only the rows the search leaves, and
|
|
10333
10694
|
// the hidden columns stay out of the file), and the groups that match
|
|
10334
|
-
// 'any';
|
|
10335
|
-
//
|
|
10336
|
-
//
|
|
10337
|
-
// for the whole result.
|
|
10695
|
+
// 'any'; a table's layout as the viewer changed it; and its title as the
|
|
10696
|
+
// header reads it (shownTitle), which the server names the file after. A
|
|
10697
|
+
// note the server returns (the file was cut) is shown before the download
|
|
10698
|
+
// starts, so a short file never passes for the whole result.
|
|
10338
10699
|
exportCsv() {
|
|
10339
10700
|
const filters = this._filterState.resolveForComponent(this.component);
|
|
10340
|
-
const
|
|
10701
|
+
const layout = this.component.type === 'grid' ? this._filterState.gridSessionLayout(this.component) : null;
|
|
10702
|
+
const state = { ...this._filterState.tileState(this.component), ...(layout ? { layout } : {}) };
|
|
10341
10703
|
const title = this.shownTitle;
|
|
10342
10704
|
this._process.download(`Exporting ${title || 'component'}`, this._reportData.exportComponent(this.reportId, this.component.id, filters, state, title)
|
|
10343
10705
|
.pipe(tap$1(({ notes }) => notes?.length && this._message.warning(notes.join(' '))), map$1(({ url }) => url)));
|
|
@@ -10590,11 +10952,6 @@ class ReportComponentComponent {
|
|
|
10590
10952
|
h: this.component.h,
|
|
10591
10953
|
});
|
|
10592
10954
|
}
|
|
10593
|
-
// A chart on a time x-axis — the only component the report's group-by control
|
|
10594
|
-
// applies to (isTimeSeriesChart, the browser-side mirror of the server's rule).
|
|
10595
|
-
get _isTimeSeries() {
|
|
10596
|
-
return isTimeSeriesChart(this.component);
|
|
10597
|
-
}
|
|
10598
10955
|
// Auto height (box fits its content) vs fixed (use `h` exactly). Derived from
|
|
10599
10956
|
// type, server-side: KPI tiles are auto; charts and lists are fixed — charts
|
|
10600
10957
|
// fit the box and never overflow, lists scroll inside it.
|
|
@@ -10641,9 +10998,9 @@ class ReportComponentComponent {
|
|
|
10641
10998
|
this.error.set(null);
|
|
10642
10999
|
const filters = this._filterState.resolveForComponent(this.component);
|
|
10643
11000
|
// The tile's interaction state, the one its CSV and every export carry
|
|
10644
|
-
// (tileDataState): a
|
|
10645
|
-
// one) so the backend re-buckets, a chart's drill
|
|
10646
|
-
// that match 'any'.
|
|
11001
|
+
// (tileDataState): the grain of a tile that follows the group-by (when
|
|
11002
|
+
// the viewer picked one) so the backend re-buckets, a chart's drill
|
|
11003
|
+
// path, and the groups that match 'any'.
|
|
10647
11004
|
const state = this._filterState.tileState(this.component);
|
|
10648
11005
|
return this._reportData.componentData(this.reportId, this.component.id, filters, state);
|
|
10649
11006
|
}
|
|
@@ -13063,6 +13420,52 @@ function nameInputId(groupId) {
|
|
|
13063
13420
|
return `filterName${groupId}`;
|
|
13064
13421
|
}
|
|
13065
13422
|
|
|
13423
|
+
// A table tile as the PDF and the deck draw it: the printed table
|
|
13424
|
+
// readGridTable() reads (GridTable) as heading rows, body rows and a total
|
|
13425
|
+
// row of plain cells, and the lines its notes add. Pure, so both renderers
|
|
13426
|
+
// draw one table one way.
|
|
13427
|
+
// The most pivot columns a file draws, from the first.
|
|
13428
|
+
const GRID_EXPORT_COLUMNS = 60;
|
|
13429
|
+
// The table's heading, body and total row as cells. A group's label carries
|
|
13430
|
+
// its row count after it, as the screen writes it.
|
|
13431
|
+
function gridExportTable(table) {
|
|
13432
|
+
const numeric = table.columns.map((column) => column.numeric);
|
|
13433
|
+
const head = Array.from({ length: Math.max(1, table.headingRows) }, (_, row) => table.headings
|
|
13434
|
+
.filter((heading) => heading.row === row)
|
|
13435
|
+
.sort((a, b) => a.column - b.column)
|
|
13436
|
+
.map((heading) => exportCell$1(headingText(heading), true, 0, heading.numeric, heading.columnSpan, heading.rowSpan)));
|
|
13437
|
+
const line = (row) => row.cells.map((cell, index) => {
|
|
13438
|
+
const isLabel = index === 0 && table.groupColumn;
|
|
13439
|
+
return exportCell$1(isLabel && row.count ? `${cell.text} (${row.count})` : cell.text, row.kind !== GridRowKind.Detail || !!table.columns[index]?.emphasis, isLabel ? row.depth : 0, numeric[index] ?? false);
|
|
13440
|
+
});
|
|
13441
|
+
return {
|
|
13442
|
+
head,
|
|
13443
|
+
body: table.rows.map(line),
|
|
13444
|
+
foot: table.totalRow ? line(table.totalRow) : null,
|
|
13445
|
+
numeric,
|
|
13446
|
+
moreRows: table.moreRows,
|
|
13447
|
+
};
|
|
13448
|
+
}
|
|
13449
|
+
// The lines a table adds under its box: the screen's notes, the pivot
|
|
13450
|
+
// columns it leaves out, and each column filter of the layout it was read in.
|
|
13451
|
+
function gridExportNotes(table, layout, locale) {
|
|
13452
|
+
const fields = new Map(table.fields.map((field) => [field.name, field]));
|
|
13453
|
+
const filters = (layout.filters ?? [])
|
|
13454
|
+
.map((filter) => gridFilterLineText(fields.get(filter.field)?.label ?? filter.field, filter, fields.get(filter.field), locale))
|
|
13455
|
+
.filter((text) => !!text);
|
|
13456
|
+
return [
|
|
13457
|
+
...table.notes,
|
|
13458
|
+
...(table.shownColumns < table.columnCount ? [`Showing the first ${table.shownColumns} of ${table.columnCount} columns`] : []),
|
|
13459
|
+
...filters,
|
|
13460
|
+
];
|
|
13461
|
+
}
|
|
13462
|
+
function headingText(heading) {
|
|
13463
|
+
return heading.hint ? `${heading.label} (${heading.hint})` : heading.label;
|
|
13464
|
+
}
|
|
13465
|
+
function exportCell$1(text, bold, indent, numeric, colSpan = 1, rowSpan = 1) {
|
|
13466
|
+
return { text, colSpan, rowSpan, bold, indent, numeric };
|
|
13467
|
+
}
|
|
13468
|
+
|
|
13066
13469
|
// How far a KPI card's rows shrink to fit a short tile. A tile shorter still
|
|
13067
13470
|
// lets the card run past its foot rather than print it unreadably small.
|
|
13068
13471
|
const KPI_EXPORT_MIN_SCALE = 0.5;
|
|
@@ -13103,8 +13506,11 @@ function emptyLabel(item) {
|
|
|
13103
13506
|
// the matching rows: 'Orders (matching "north")'. An untitled one reads
|
|
13104
13507
|
// 'Matching "north"'. '' when there is nothing to print. Shared by the PDF
|
|
13105
13508
|
// and the deck, continuation pages included.
|
|
13106
|
-
function exportTitle(component, tokens, search, crumbs) {
|
|
13509
|
+
function exportTitle(component, tokens, search, crumbs, filtered = false) {
|
|
13107
13510
|
const title = crumbedTitle(fillTitleTokens(component.title, tokens), crumbs ?? []);
|
|
13511
|
+
if (filtered) {
|
|
13512
|
+
return title ? `${title} (filtered)` : 'Filtered';
|
|
13513
|
+
}
|
|
13108
13514
|
if (!search) {
|
|
13109
13515
|
return title;
|
|
13110
13516
|
}
|
|
@@ -13236,7 +13642,16 @@ function exportWindowsLine(item, timezone, locale) {
|
|
|
13236
13642
|
// descriptions are in, since a file has no tooltip or header to carry them.
|
|
13237
13643
|
// Empty when the data has none of the keys.
|
|
13238
13644
|
function exportNoteLines(item, timezone, locale) {
|
|
13239
|
-
return
|
|
13645
|
+
return [
|
|
13646
|
+
...componentNoteLines(item.data, { locale, timezone }, { windows: false, windowDescriptions: true }),
|
|
13647
|
+
...(item.tableNotes ?? []),
|
|
13648
|
+
];
|
|
13649
|
+
}
|
|
13650
|
+
// Whether a tile has nothing to draw: no rows, or a table with no line.
|
|
13651
|
+
function isEmptyTile(item) {
|
|
13652
|
+
return item.table
|
|
13653
|
+
? !item.table.rows.length && !item.table.totalRow
|
|
13654
|
+
: !item.data?.rows?.length;
|
|
13240
13655
|
}
|
|
13241
13656
|
// Whether a KPI card has more to draw than its value and label. Without, both
|
|
13242
13657
|
// exports draw the tile exactly as they always have.
|
|
@@ -13429,7 +13844,10 @@ class ReportExportCollectorService {
|
|
|
13429
13844
|
filters: Object.fromEntries(tiles.map((tile) => [tile.id, this._filterState.resolveForComponent(tile)])),
|
|
13430
13845
|
valueFilters: Object.fromEntries(values
|
|
13431
13846
|
.map((value) => [value.id, this._filterState.resolveForComponent(value)])),
|
|
13432
|
-
states: Object.fromEntries(tiles.map((tile) => [tile.id,
|
|
13847
|
+
states: Object.fromEntries(tiles.map((tile) => [tile.id, this._tileState(report, tile, state)])),
|
|
13848
|
+
gridExpansions: Object.fromEntries(tiles
|
|
13849
|
+
.map((tile) => [tile.id, this._filterState.gridExpansion(tile)])
|
|
13850
|
+
.filter(([, expansion]) => !!expansion)),
|
|
13433
13851
|
frequency: this._filterState.frequency(),
|
|
13434
13852
|
features: offeredViewerFeatures(report.viewerFeatures),
|
|
13435
13853
|
formatContext: this._filterState.formatContext(),
|
|
@@ -13513,10 +13931,14 @@ class ReportExportCollectorService {
|
|
|
13513
13931
|
return of({ component, data: null, hiddenColumns: [] });
|
|
13514
13932
|
}
|
|
13515
13933
|
const filters = snapshot.filters[component.id] ?? [];
|
|
13934
|
+
if (component.type === 'grid') {
|
|
13935
|
+
return this._gridTable(report, component, filters, snapshot.states[component.id] ?? {}, listRows, snapshot);
|
|
13936
|
+
}
|
|
13516
13937
|
// The tile's own state, the one its screen and its CSV carry
|
|
13517
|
-
// (tileDataState): a
|
|
13518
|
-
//
|
|
13519
|
-
// the screen's rows. A list adds its
|
|
13938
|
+
// (tileDataState): a tile that follows the group-by re-buckets at the
|
|
13939
|
+
// viewer's grain, a time-series chart keeps its drill path, a list reads
|
|
13940
|
+
// in the screen's order and holds the screen's rows. A list adds its
|
|
13941
|
+
// export paging.
|
|
13520
13942
|
const tileState = snapshot.states[component.id] ?? {};
|
|
13521
13943
|
const state = component.type === 'list'
|
|
13522
13944
|
? { ...tileState, page: 1, pageSize: listRows }
|
|
@@ -13531,6 +13953,47 @@ class ReportExportCollectorService {
|
|
|
13531
13953
|
return this._reportData.componentData(report.id, component.id, filters, state, true, { handlers: false })
|
|
13532
13954
|
.pipe(map$1((data) => this._collected(component, data, state.search, hiddenColumns, snapshot)), catchError(() => of({ component, data: null, hiddenColumns })));
|
|
13533
13955
|
}
|
|
13956
|
+
// A tile's state as an export reads it: tileDataState, and for a table the
|
|
13957
|
+
// layout the viewer put it in, absent while it shows its stored one.
|
|
13958
|
+
_tileState(report, tile, state) {
|
|
13959
|
+
const layout = tile.type === 'grid' ? this._filterState.gridSessionLayout(tile) : null;
|
|
13960
|
+
return { ...tileDataState(report, tile, state), ...(layout ? { layout } : {}) };
|
|
13961
|
+
}
|
|
13962
|
+
// A table tile as the screen draws it, read apart from the screen through
|
|
13963
|
+
// an export source (readGridTable()): in the viewer's layout with the
|
|
13964
|
+
// groups they left open, at most `rows` lines and GRID_EXPORT_COLUMNS
|
|
13965
|
+
// pivot columns, each value written by the screen's formatter once the
|
|
13966
|
+
// read's report line is in. That line stands in as its data, so the title,
|
|
13967
|
+
// the subtitle and the notes read it as a list's. A failed read is a
|
|
13968
|
+
// failed tile.
|
|
13969
|
+
_gridTable(report, component, filters, state, rows, snapshot) {
|
|
13970
|
+
const locale = snapshot.formatContext.locale;
|
|
13971
|
+
const source = new ReportGridSource(this._reportData, report.id, component.id, { isExport: true, locale: () => locale });
|
|
13972
|
+
const screen = () => reportGridFormatter({
|
|
13973
|
+
format: snapshot.formatContext,
|
|
13974
|
+
timezone: this._filterState.timezone(),
|
|
13975
|
+
report: source.report(),
|
|
13976
|
+
});
|
|
13977
|
+
const formatter = {
|
|
13978
|
+
format: (value, field, placement) => screen().format(value, field, placement),
|
|
13979
|
+
toNumber: (text, field, placement) => screen().toNumber(text, field, placement),
|
|
13980
|
+
};
|
|
13981
|
+
const layout = state.layout ?? authoredGridLayout(component);
|
|
13982
|
+
const params = {
|
|
13983
|
+
filters,
|
|
13984
|
+
...(state.matchAnyGroupIds?.length ? { matchAnyGroupIds: state.matchAnyGroupIds } : {}),
|
|
13985
|
+
...(state.frequency ? { frequency: state.frequency } : {}),
|
|
13986
|
+
};
|
|
13987
|
+
return readGridTable(source, layout, params, snapshot.gridExpansions?.[component.id], { rows, columns: GRID_EXPORT_COLUMNS }, formatter)
|
|
13988
|
+
.pipe(map$1((table) => ({
|
|
13989
|
+
component,
|
|
13990
|
+
data: { ...(source.report() ?? {}), columns: [], rows: [] },
|
|
13991
|
+
hiddenColumns: [],
|
|
13992
|
+
table,
|
|
13993
|
+
tableNotes: gridExportNotes(table, layout, locale),
|
|
13994
|
+
...((layout.filters ?? []).length ? { filtered: true } : {}),
|
|
13995
|
+
})), catchError(() => of({ component, data: null, hiddenColumns: [] })));
|
|
13996
|
+
}
|
|
13534
13997
|
// One tile's answer as the renderers take it. A search the server could
|
|
13535
13998
|
// not apply (searchNote: the list has no text column) narrowed nothing, so
|
|
13536
13999
|
// the item does not carry it. A drilled chart carries the breadcrumb of the
|
|
@@ -13675,7 +14138,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
13675
14138
|
// in the report's format context, as on screen.
|
|
13676
14139
|
async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
|
|
13677
14140
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
13678
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
14141
|
+
const echarts = (await import('./firestitch-report-echarts-DMflI6jp.mjs')).default;
|
|
13679
14142
|
const host = document.createElement('div');
|
|
13680
14143
|
host.style.width = `${width}px`;
|
|
13681
14144
|
host.style.height = `${height}px`;
|
|
@@ -13995,6 +14458,8 @@ const LIST_INDENT = 16;
|
|
|
13995
14458
|
// What the export's overflow gate says the PDF does with a text block that does
|
|
13996
14459
|
// not fit its box.
|
|
13997
14460
|
const PDF_OVERFLOW_NOTE = 'The PDF cuts it off at the bottom of its box.';
|
|
14461
|
+
// How far a table's group label steps in per level, points.
|
|
14462
|
+
const GRID_INDENT$1 = 8;
|
|
13998
14463
|
// Builds the PDF client-side with jspdf. The report's unit is inches and PDFs
|
|
13999
14464
|
// are points at exactly 72/inch, so each report page becomes one PDF page of
|
|
14000
14465
|
// the exact physical size and components land at their precise positions.
|
|
@@ -14436,7 +14901,7 @@ class ReportPdfService {
|
|
|
14436
14901
|
const y = component.y * PT;
|
|
14437
14902
|
const w = component.w * PT;
|
|
14438
14903
|
const h = component.h * PT;
|
|
14439
|
-
const title = exportTitle(component, tokens, item.search, item.crumbs);
|
|
14904
|
+
const title = exportTitle(component, tokens, item.search, item.crumbs, item.filtered);
|
|
14440
14905
|
if (title) {
|
|
14441
14906
|
this._componentTitle(doc, title, x, y, w);
|
|
14442
14907
|
}
|
|
@@ -14457,7 +14922,7 @@ class ReportPdfService {
|
|
|
14457
14922
|
kept.forEach((line, index) => {
|
|
14458
14923
|
this._smallLine(doc, this._pdfText(line), x, bodyY + bodyH + index * NOTE_LINE_H$1, w, 'left', 'italic');
|
|
14459
14924
|
});
|
|
14460
|
-
if (!item.data ||
|
|
14925
|
+
if (!item.data || isEmptyTile(item)) {
|
|
14461
14926
|
this._boxNote(doc, emptyLabel(item), x, bodyY, w, bodyH);
|
|
14462
14927
|
return;
|
|
14463
14928
|
}
|
|
@@ -14468,6 +14933,7 @@ class ReportPdfService {
|
|
|
14468
14933
|
break;
|
|
14469
14934
|
}
|
|
14470
14935
|
case 'list':
|
|
14936
|
+
case 'grid':
|
|
14471
14937
|
this._renderTable(context, item, x, bodyY, w, bodyH, overflows);
|
|
14472
14938
|
break;
|
|
14473
14939
|
case 'kpi':
|
|
@@ -14520,12 +14986,8 @@ class ReportPdfService {
|
|
|
14520
14986
|
// A totals row closes the table once: in the box when every row fits it,
|
|
14521
14987
|
// else after the last continuation row. Points throughout.
|
|
14522
14988
|
_renderTable(context, item, x, y, w, h, overflows) {
|
|
14523
|
-
const { component, search,
|
|
14524
|
-
const
|
|
14525
|
-
const head = listHeadings(component, data, context.formatContext.locale, hiddenColumns)
|
|
14526
|
-
.map((heading) => this._pdfText(heading));
|
|
14527
|
-
const { cells, numeric, foot } = this._tableCells(context, component, data, hiddenColumns);
|
|
14528
|
-
const omitted = Math.max(0, (data.paging?.total ?? cells.length) - cells.length);
|
|
14989
|
+
const { component, search, filtered } = item;
|
|
14990
|
+
const { head, cells, numeric, foot, omitted } = item.table ? this._gridCells(item) : this._listCells(context, item);
|
|
14529
14991
|
const bottom = context.pageH - (y + h);
|
|
14530
14992
|
const { fit, columnStyles } = this._measure(context, head, cells, numeric, foot, x, y, w, bottom);
|
|
14531
14993
|
if (fit === cells.length && omitted === 0) {
|
|
@@ -14538,16 +15000,46 @@ class ReportPdfService {
|
|
|
14538
15000
|
// page.
|
|
14539
15001
|
const kept = Math.max(0, fit - 1);
|
|
14540
15002
|
overflows.push({
|
|
14541
|
-
component, search, crumbs: item.crumbs, head, numeric, rows: cells.slice(kept), omitted, foot, x, w, columnStyles,
|
|
15003
|
+
component, search, crumbs: item.crumbs, filtered, head, numeric, rows: cells.slice(kept), omitted, foot, x, w, columnStyles,
|
|
14542
15004
|
});
|
|
14543
15005
|
if (fit >= 1) {
|
|
14544
|
-
const body = [...cells.slice(0, kept), this._noticeRow(CONTINUED_NOTICE,
|
|
15006
|
+
const body = [...cells.slice(0, kept), this._noticeRow(CONTINUED_NOTICE, numeric.length, true)];
|
|
14545
15007
|
this._drawTable(context, this._tableOptions(context, head, numeric, body, x, y, w, bottom, columnStyles));
|
|
14546
15008
|
}
|
|
14547
15009
|
else {
|
|
14548
15010
|
this._boxNote(context.doc, CONTINUED_NOTICE, x, y, w, h);
|
|
14549
15011
|
}
|
|
14550
15012
|
}
|
|
15013
|
+
// A list's heading, rows, totals row and the rows past its fetch.
|
|
15014
|
+
_listCells(context, item) {
|
|
15015
|
+
const { component, hiddenColumns } = item;
|
|
15016
|
+
const data = item.data;
|
|
15017
|
+
const head = [listHeadings(component, data, context.formatContext.locale, hiddenColumns).map((heading) => this._pdfText(heading))];
|
|
15018
|
+
const { cells, numeric, foot } = this._tableCells(context, component, data, hiddenColumns);
|
|
15019
|
+
return { head, cells, numeric, foot, omitted: Math.max(0, (data.paging?.total ?? cells.length) - cells.length) };
|
|
15020
|
+
}
|
|
15021
|
+
// A table tile's heading rows (spanned as the screen's header), its lines
|
|
15022
|
+
// (groups and totals bold, a group's label stepped in by its depth) and its
|
|
15023
|
+
// total row, as gridExportTable() lays them out, and the lines past its cap.
|
|
15024
|
+
_gridCells(item) {
|
|
15025
|
+
const table = gridExportTable(item.table);
|
|
15026
|
+
const cell = (exported) => ({
|
|
15027
|
+
content: this._pdfText(exported.text),
|
|
15028
|
+
colSpan: exported.colSpan,
|
|
15029
|
+
rowSpan: exported.rowSpan,
|
|
15030
|
+
styles: {
|
|
15031
|
+
...(exported.bold ? { fontStyle: 'bold' } : {}),
|
|
15032
|
+
...(exported.indent ? { cellPadding: { top: 3, right: 3, bottom: 3, left: 3 + exported.indent * GRID_INDENT$1 } } : {}),
|
|
15033
|
+
},
|
|
15034
|
+
});
|
|
15035
|
+
return {
|
|
15036
|
+
head: table.head.map((row) => row.map(cell)),
|
|
15037
|
+
cells: table.body.map((row) => row.map(cell)),
|
|
15038
|
+
numeric: table.numeric,
|
|
15039
|
+
foot: table.foot ? table.foot.map((exported) => this._pdfText(exported.text)) : null,
|
|
15040
|
+
omitted: table.moreRows,
|
|
15041
|
+
};
|
|
15042
|
+
}
|
|
14551
15043
|
// Each fetched row as the strings its table cells print, in the report's
|
|
14552
15044
|
// zone, locale and currency, which columns hold formatted numbers, and the
|
|
14553
15045
|
// totals row (null without one). The columns the viewer hid are left out.
|
|
@@ -14574,7 +15066,7 @@ class ReportPdfService {
|
|
|
14574
15066
|
_tableOptions(context, head, numeric, body, x, startY, w, bottom, columnStyles, foot) {
|
|
14575
15067
|
return {
|
|
14576
15068
|
theme: 'grid',
|
|
14577
|
-
head
|
|
15069
|
+
head,
|
|
14578
15070
|
body,
|
|
14579
15071
|
startY,
|
|
14580
15072
|
margin: { left: x, right: context.pageW - x - w, bottom },
|
|
@@ -14686,7 +15178,7 @@ class ReportPdfService {
|
|
|
14686
15178
|
const bottom = this._continuationMargin(bands.footerHeight);
|
|
14687
15179
|
const { x, w } = overflow;
|
|
14688
15180
|
const body = overflow.omitted > 0
|
|
14689
|
-
? [...overflow.rows, this._noticeRow(moreRowsNotice(overflow.omitted), overflow.
|
|
15181
|
+
? [...overflow.rows, this._noticeRow(moreRowsNotice(overflow.omitted), overflow.numeric.length, false)]
|
|
14690
15182
|
: overflow.rows;
|
|
14691
15183
|
// The document's pages the table draws on, in order.
|
|
14692
15184
|
const pages = [];
|
|
@@ -14699,7 +15191,7 @@ class ReportPdfService {
|
|
|
14699
15191
|
willDrawPage: () => {
|
|
14700
15192
|
pages.push(doc.getCurrentPageInfo().pageNumber);
|
|
14701
15193
|
const tokens = this._tokens(context, page, index, pageNumber + pages.length);
|
|
14702
|
-
this._componentTitle(doc, this._continuedTitle(overflow.component, tokens, overflow.search, overflow.crumbs), x, top, w);
|
|
15194
|
+
this._componentTitle(doc, this._continuedTitle(overflow.component, tokens, overflow.search, overflow.crumbs, overflow.filtered), x, top, w);
|
|
14703
15195
|
},
|
|
14704
15196
|
});
|
|
14705
15197
|
await this._renderContinuationMasters(context, page, index, pageNumber, pages);
|
|
@@ -14722,8 +15214,8 @@ class ReportPdfService {
|
|
|
14722
15214
|
_continuationMargin(band) {
|
|
14723
15215
|
return band ? band * PT + CONTINUATION_GAP : NO_BAND_MARGIN;
|
|
14724
15216
|
}
|
|
14725
|
-
_continuedTitle(component, tokens, search, crumbs) {
|
|
14726
|
-
const title = exportTitle(component, tokens, search, crumbs);
|
|
15217
|
+
_continuedTitle(component, tokens, search, crumbs, filtered) {
|
|
15218
|
+
const title = exportTitle(component, tokens, search, crumbs, filtered);
|
|
14727
15219
|
return title ? `${title} (continued)` : 'Continued';
|
|
14728
15220
|
}
|
|
14729
15221
|
// A KPI tile: the card the screen draws (kpi-card.ts decides every line),
|
|
@@ -14963,6 +15455,9 @@ const PPTX_RANK_WHOLE_STEP_PEAK = 10;
|
|
|
14963
15455
|
const PPTX_RANK_FORMAT_CODE = '0';
|
|
14964
15456
|
// Approximate table row height, inches — drives the rows-that-fit cap.
|
|
14965
15457
|
const SLIDE_ROW_H = 0.26;
|
|
15458
|
+
// The spaces a table's group label steps in by per level: a cell's text is
|
|
15459
|
+
// the one place a slide table indents.
|
|
15460
|
+
const GRID_INDENT = '\u00A0\u00A0\u00A0';
|
|
14966
15461
|
// Rows fetched for a list's slide table. A slide table never continues onto
|
|
14967
15462
|
// another slide, so this only has to cover the rows a box can show; the
|
|
14968
15463
|
// moreRowsNotice() row names the rest.
|
|
@@ -15350,7 +15845,7 @@ class ReportPptxService {
|
|
|
15350
15845
|
const component = item.component;
|
|
15351
15846
|
// Geometry is in inches — PowerPoint's native unit.
|
|
15352
15847
|
const { x, y, w, h } = component;
|
|
15353
|
-
const title = exportTitle(component, tokens, item.search, item.crumbs);
|
|
15848
|
+
const title = exportTitle(component, tokens, item.search, item.crumbs, item.filtered);
|
|
15354
15849
|
if (title) {
|
|
15355
15850
|
slide.addText(title, {
|
|
15356
15851
|
x, y, w, h: TITLE_H,
|
|
@@ -15380,7 +15875,7 @@ class ReportPptxService {
|
|
|
15380
15875
|
fontSize: SMALL_FONT_SIZE, color: MUTED_COLOR, valign: 'top', margin: 0, fit: 'none',
|
|
15381
15876
|
});
|
|
15382
15877
|
}
|
|
15383
|
-
if (!item.data ||
|
|
15878
|
+
if (!item.data || isEmptyTile(item)) {
|
|
15384
15879
|
slide.addText(emptyLabel(item), {
|
|
15385
15880
|
x, y: bodyY, w, h: bodyH,
|
|
15386
15881
|
fontSize: 10, italic: true, color: MUTED_COLOR, align: 'center', valign: 'middle',
|
|
@@ -15394,6 +15889,9 @@ class ReportPptxService {
|
|
|
15394
15889
|
case 'list':
|
|
15395
15890
|
this._renderTable(slide, component, item.data, item.hiddenColumns, timezone, context, x, bodyY, w, bodyH);
|
|
15396
15891
|
break;
|
|
15892
|
+
case 'grid':
|
|
15893
|
+
this._renderGridTable(slide, item, context, x, bodyY, w, bodyH);
|
|
15894
|
+
break;
|
|
15397
15895
|
case 'kpi':
|
|
15398
15896
|
this._renderKpi(pptx, slide, item, timezone, context, x, bodyY, w, bodyH);
|
|
15399
15897
|
break;
|
|
@@ -15993,6 +16491,43 @@ class ReportPptxService {
|
|
|
15993
16491
|
valign: 'middle',
|
|
15994
16492
|
});
|
|
15995
16493
|
}
|
|
16494
|
+
// A table tile as a native table (gridExportTable()): its heading rows
|
|
16495
|
+
// spanned as the screen's header, as many lines as the box holds (groups
|
|
16496
|
+
// and totals bold, a group's label stepped in by its depth), the
|
|
16497
|
+
// moreRowsNotice row for the rest and the total row. A number is written
|
|
16498
|
+
// in the report's locale.
|
|
16499
|
+
_renderGridTable(slide, item, context, x, y, w, h) {
|
|
16500
|
+
const table = gridExportTable(item.table);
|
|
16501
|
+
const cell = (exported, options = {}) => ({
|
|
16502
|
+
text: `${GRID_INDENT.repeat(exported.indent)}${exported.text}`,
|
|
16503
|
+
options: pptxRunOptions({
|
|
16504
|
+
...(exported.colSpan > 1 ? { colspan: exported.colSpan } : {}),
|
|
16505
|
+
...(exported.rowSpan > 1 ? { rowspan: exported.rowSpan } : {}),
|
|
16506
|
+
...(exported.bold ? { bold: true } : {}),
|
|
16507
|
+
...(exported.numeric ? { align: 'right' } : {}),
|
|
16508
|
+
...options,
|
|
16509
|
+
}, exported.numeric ? context.locale : undefined),
|
|
16510
|
+
});
|
|
16511
|
+
const cap = Math.max(1, Math.floor(h / SLIDE_ROW_H) - table.head.length - (table.foot ? 1 : 0));
|
|
16512
|
+
const lines = table.body.slice(0, cap);
|
|
16513
|
+
const remaining = table.body.length - lines.length + table.moreRows;
|
|
16514
|
+
slide.addTable([
|
|
16515
|
+
...table.head.map((row) => row.map((heading) => cell(heading, { color: 'FFFFFF', fill: { color: '4E79A7' } }))),
|
|
16516
|
+
...lines.map((row) => row.map((line) => cell(line))),
|
|
16517
|
+
...(remaining > 0
|
|
16518
|
+
? [[{ text: moreRowsNotice(remaining), options: { colspan: table.numeric.length, italic: true, color: MUTED_COLOR } }]]
|
|
16519
|
+
: []),
|
|
16520
|
+
...(table.foot ? [table.foot.map((total) => cell(total, { bold: true, fill: { color: 'E4E7EB' } }))] : []),
|
|
16521
|
+
], {
|
|
16522
|
+
x, y, w,
|
|
16523
|
+
fontSize: 9,
|
|
16524
|
+
color: '1F2933',
|
|
16525
|
+
border: { type: 'solid', pt: 0.5, color: 'E4E7EB' },
|
|
16526
|
+
autoPage: false,
|
|
16527
|
+
rowH: SLIDE_ROW_H,
|
|
16528
|
+
valign: 'middle',
|
|
16529
|
+
});
|
|
16530
|
+
}
|
|
15996
16531
|
// A KPI tile: the card the screen draws (kpi-card.ts decides every line),
|
|
15997
16532
|
// as native text boxes and shapes stacked and centered in the body. A card
|
|
15998
16533
|
// with only its value and label keeps the tile's long-standing single text
|
|
@@ -16619,7 +17154,7 @@ class ReportComponent {
|
|
|
16619
17154
|
const filters = this._filterState.resolveForReport(report, pageId);
|
|
16620
17155
|
const frequency = this._filterState.frequency();
|
|
16621
17156
|
const snapshot = this._collector.snapshot(report);
|
|
16622
|
-
const states = zipExportStates(report, snapshot.state, pageId);
|
|
17157
|
+
const states = zipExportStates(report, snapshot.state, pageId, (component) => this._filterState.gridSessionLayout(component));
|
|
16623
17158
|
this._process.download('Exporting data', this._collector.exportWording(report, snapshot, pageId)
|
|
16624
17159
|
.pipe(switchMap(({ lines, titles }) => this._reportData.exportReport(report.id, filters, pageId, frequency, lines, states, titles))));
|
|
16625
17160
|
}
|
|
@@ -16808,7 +17343,7 @@ class ReportComponent {
|
|
|
16808
17343
|
ReportExportService,
|
|
16809
17344
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
16810
17345
|
// this route's chunk.
|
|
16811
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
17346
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DMflI6jp.mjs').then((module) => module.default) }),
|
|
16812
17347
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
16813
17348
|
// presets; a preset only sets the days.
|
|
16814
17349
|
provideReportDatePicker(),
|
|
@@ -16829,7 +17364,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
16829
17364
|
ReportExportService,
|
|
16830
17365
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
16831
17366
|
// this route's chunk.
|
|
16832
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
17367
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DMflI6jp.mjs').then((module) => module.default) }),
|
|
16833
17368
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
16834
17369
|
// presets; a preset only sets the days.
|
|
16835
17370
|
provideReportDatePicker(),
|
|
@@ -18491,7 +19026,7 @@ class FsAiReportComponent {
|
|
|
18491
19026
|
const filters = this._filterState.resolveForReport(report, pageId);
|
|
18492
19027
|
const frequency = this._filterState.frequency();
|
|
18493
19028
|
const snapshot = this._collector.snapshot(report);
|
|
18494
|
-
const states = zipExportStates(report, snapshot.state, pageId);
|
|
19029
|
+
const states = zipExportStates(report, snapshot.state, pageId, (component) => this._filterState.gridSessionLayout(component));
|
|
18495
19030
|
this._process.download('Exporting data', this._collector.exportWording(report, snapshot, pageId)
|
|
18496
19031
|
.pipe(switchMap(({ lines, titles }) => this._reportData.exportReport(report.id, filters, pageId, frequency, lines, states, titles))));
|
|
18497
19032
|
}
|
|
@@ -18805,7 +19340,7 @@ class FsAiReportComponent {
|
|
|
18805
19340
|
ReportAiChatData,
|
|
18806
19341
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
18807
19342
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
18808
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19343
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DMflI6jp.mjs').then((module) => module.default) }),
|
|
18809
19344
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
18810
19345
|
// presets; a preset only sets the days.
|
|
18811
19346
|
provideReportDatePicker(),
|
|
@@ -18829,7 +19364,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
18829
19364
|
ReportAiChatData,
|
|
18830
19365
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
18831
19366
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
18832
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
19367
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DMflI6jp.mjs').then((module) => module.default) }),
|
|
18833
19368
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
18834
19369
|
// presets; a preset only sets the days.
|
|
18835
19370
|
provideReportDatePicker(),
|
|
@@ -18869,4 +19404,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
18869
19404
|
*/
|
|
18870
19405
|
|
|
18871
19406
|
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 };
|
|
18872
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
19407
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-C94rjx24.mjs.map
|