@firestitch/report 18.10.0 → 18.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/reports/data/report.data.d.ts +3 -5
- package/app/reports/dialogs/report-settings/report-settings.component.d.ts +30 -2
- package/app/reports/filter-settings-rules.d.ts +4 -0
- package/app/reports/interfaces/report-filter-group-changes.interface.d.ts +7 -0
- package/app/reports/interfaces/report.interface.d.ts +1 -14
- package/app/reports/report-filter-items.d.ts +2 -8
- package/app/reports/report-view-config.d.ts +1 -3
- package/app/reports/services/report-filter-state.service.d.ts +5 -14
- package/app/reports/services/report-view.service.d.ts +1 -13
- package/app/reports/views/report/fs-ai-report.component.d.ts +2 -8
- package/esm2022/app/reports/canvas-landing-page.mjs +7 -6
- package/esm2022/app/reports/components/component-list/component-list.component.mjs +5 -5
- package/esm2022/app/reports/components/report-canvas/report-canvas.component.mjs +13 -12
- package/esm2022/app/reports/components/report-component/report-component.component.mjs +9 -9
- package/esm2022/app/reports/data/report.data.mjs +8 -5
- package/esm2022/app/reports/dialogs/report-settings/report-settings.component.mjs +169 -21
- package/esm2022/app/reports/filter-settings-rules.mjs +8 -1
- package/esm2022/app/reports/interfaces/canvas-landing-input.interface.mjs +1 -1
- package/esm2022/app/reports/interfaces/report-filter-group-changes.interface.mjs +2 -0
- package/esm2022/app/reports/interfaces/report-temporary-state.interface.mjs +1 -1
- package/esm2022/app/reports/interfaces/report-view-config.interface.mjs +1 -1
- package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
- package/esm2022/app/reports/report-date-presets.mjs +2 -2
- package/esm2022/app/reports/report-filter-items.mjs +25 -105
- package/esm2022/app/reports/report-view-config.mjs +6 -116
- package/esm2022/app/reports/services/report-filter-state.service.mjs +92 -269
- package/esm2022/app/reports/services/report-view.service.mjs +25 -165
- package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +56 -120
- package/esm2022/app/reports/views/report/report.component.mjs +11 -8
- package/fesm2022/{firestitch-report-echarts-UBg8Wlhd.mjs → firestitch-report-echarts-DHEH_K_q.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-UBg8Wlhd.mjs.map → firestitch-report-echarts-DHEH_K_q.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-B1keNrcK.mjs → firestitch-report-firestitch-report-ZcHTnNKq.mjs} +454 -1470
- package/fesm2022/firestitch-report-firestitch-report-ZcHTnNKq.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/app/reports/interfaces/report-url-link.interface.d.ts +0 -4
- package/app/reports/interfaces/report-url-value-name.type.d.ts +0 -1
- package/app/reports/interfaces/report-url-write.interface.d.ts +0 -4
- package/app/reports/report-url-state.d.ts +0 -36
- package/esm2022/app/reports/interfaces/report-url-link.interface.mjs +0 -2
- package/esm2022/app/reports/interfaces/report-url-value-name.type.mjs +0 -2
- package/esm2022/app/reports/interfaces/report-url-write.interface.mjs +0 -2
- package/esm2022/app/reports/report-url-state.mjs +0 -677
- package/fesm2022/firestitch-report-firestitch-report-B1keNrcK.mjs.map +0 -1
|
@@ -12,12 +12,12 @@ import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
|
|
|
12
12
|
import * as i2$2 from '@firestitch/autocomplete-chips';
|
|
13
13
|
import { FsAutocompleteChipsModule } from '@firestitch/autocomplete-chips';
|
|
14
14
|
import * as i1$2 from '@firestitch/filter';
|
|
15
|
-
import {
|
|
15
|
+
import { ItemType, getExcludeName, FsFilterModule } from '@firestitch/filter';
|
|
16
16
|
import * as i2 from '@firestitch/menu';
|
|
17
17
|
import { FsMenuModule, FsMenuComponent } from '@firestitch/menu';
|
|
18
18
|
import { FsProcess } from '@firestitch/process';
|
|
19
19
|
import { FsPrompt } from '@firestitch/prompt';
|
|
20
|
-
import { map, catchError, shareReplay, scan, filter, take, tap, switchMap, finalize,
|
|
20
|
+
import { map, catchError, shareReplay, scan, filter, take, tap, switchMap, finalize, concatMap, toArray, mergeMap, takeUntil } from 'rxjs/operators';
|
|
21
21
|
import { toSignal, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
22
22
|
import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
|
|
23
23
|
import { FsAiChatComponent, FsAiChatData } from '@firestitch/ai';
|
|
@@ -27,17 +27,15 @@ import * as i6$1 from '@firestitch/message';
|
|
|
27
27
|
import { FsMessage, FsMessageModule } from '@firestitch/message';
|
|
28
28
|
import * as i3$1 from '@firestitch/zoom-pan';
|
|
29
29
|
import { FsZoomPanComponent, FsZoomPanModule } from '@firestitch/zoom-pan';
|
|
30
|
-
import { forkJoin, of, Subject, defer, merge, map as map$1, switchMap as switchMap$1, timer, catchError as catchError$1, filter as filter$1, tap as tap$1, debounceTime,
|
|
31
|
-
import { FsApi, DisplayApiError,
|
|
30
|
+
import { forkJoin, of, Subject, defer, merge, map as map$1, switchMap as switchMap$1, timer, catchError as catchError$1, filter as filter$1, tap as tap$1, debounceTime, from, throwError, EMPTY, BehaviorSubject } from 'rxjs';
|
|
31
|
+
import { FsApi, DisplayApiError, StreamEventType } from '@firestitch/api';
|
|
32
32
|
import { parseISO, isValid, addDays, min, max, startOfDay, isLastDayOfMonth, endOfMonth, addMonths, addYears, addQuarters, addWeeks, formatISO, format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek } from 'date-fns';
|
|
33
33
|
import { toZonedTime } from 'date-fns-tz';
|
|
34
|
-
import * as i2$3 from '@firestitch/date';
|
|
35
|
-
import { iso8601, FsDateModule } from '@firestitch/date';
|
|
36
34
|
import { MatProgressBar } from '@angular/material/progress-bar';
|
|
37
35
|
import { MatCheckbox } from '@angular/material/checkbox';
|
|
38
36
|
import * as i3 from '@firestitch/dialog';
|
|
39
37
|
import { FsDialogModule } from '@firestitch/dialog';
|
|
40
|
-
import { NgTemplateOutlet } from '@angular/common';
|
|
38
|
+
import { NgTemplateOutlet, DOCUMENT } from '@angular/common';
|
|
41
39
|
import * as i1 from '@firestitch/list';
|
|
42
40
|
import { FsListComponent, PaginationStrategy, FsListModule } from '@firestitch/list';
|
|
43
41
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
@@ -57,18 +55,20 @@ import * as i3$2 from '@firestitch/mask';
|
|
|
57
55
|
import { FsMaskModule } from '@firestitch/mask';
|
|
58
56
|
import * as i6 from '@firestitch/tabs';
|
|
59
57
|
import { FsTabsModule } from '@firestitch/tabs';
|
|
58
|
+
import { HttpContext } from '@angular/common/http';
|
|
60
59
|
import * as i9 from '@firestitch/datepicker';
|
|
61
60
|
import { FsDatePickerModule, FS_DATEPICKER_CONFIG_DEFAULT, FS_DATEPICKER_CONFIG } from '@firestitch/datepicker';
|
|
62
61
|
import * as i4 from '@firestitch/skeleton';
|
|
63
62
|
import { FsSkeletonModule } from '@firestitch/skeleton';
|
|
64
63
|
import { MatOption as MatOption$1 } from '@angular/material/core';
|
|
65
64
|
import { guid } from '@firestitch/common';
|
|
66
|
-
import { HttpContext } from '@angular/common/http';
|
|
67
65
|
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
|
68
66
|
import * as i1$4 from '@firestitch/chip';
|
|
69
67
|
import { FsChipModule } from '@firestitch/chip';
|
|
70
68
|
import * as i4$1 from '@firestitch/tree';
|
|
71
69
|
import { FsTreeComponent, TreeDragMode, TreeActionType, FsTreeModule } from '@firestitch/tree';
|
|
70
|
+
import * as i2$3 from '@firestitch/date';
|
|
71
|
+
import { FsDateModule } from '@firestitch/date';
|
|
72
72
|
import { FsMarkdownRendererComponent, FsMarkdownEditorComponent } from '@firestitch/markdown-editor';
|
|
73
73
|
import * as i1$5 from '@angular/material/expansion';
|
|
74
74
|
import { MatExpansionModule } from '@angular/material/expansion';
|
|
@@ -76,12 +76,13 @@ import { MatExpansionModule } from '@angular/material/expansion';
|
|
|
76
76
|
// Which page the canvas opens when its report input changes.
|
|
77
77
|
//
|
|
78
78
|
// - The same report with more pages than before: a page was just added in
|
|
79
|
-
// this tab, so the canvas lands on the last page. The
|
|
80
|
-
// beat it: that is
|
|
81
|
-
// - Otherwise the
|
|
82
|
-
//
|
|
79
|
+
// this tab, so the canvas lands on the last page. The asked-for page does
|
|
80
|
+
// not beat it: that is a stale value the host still holds.
|
|
81
|
+
// - Otherwise the asked-for page (a ?page= the report opened with, or the
|
|
82
|
+
// open view's), when the report has it.
|
|
83
|
+
// - Another report that asks for no page it has opens on its first page:
|
|
83
84
|
// the old report's page number means nothing there.
|
|
84
|
-
// - The same report keeps the page it shows when the
|
|
85
|
+
// - The same report keeps the page it shows when the asked-for page does not
|
|
85
86
|
// resolve (it may still be loading); an index past the last page falls
|
|
86
87
|
// back to the first.
|
|
87
88
|
function canvasLandingPage(input) {
|
|
@@ -534,10 +535,13 @@ class ReportData {
|
|
|
534
535
|
...config,
|
|
535
536
|
});
|
|
536
537
|
}
|
|
537
|
-
// Update a filter group — its exposure level ('component' | 'report' | 'both')
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
538
|
+
// Update a filter group — its exposure level ('component' | 'report' | 'both'),
|
|
539
|
+
// its name (an identifier, unique in the report; the server refuses one that
|
|
540
|
+
// breaks the rule, with a message naming the problem), its label (never
|
|
541
|
+
// changes the name) and/or its default value (the value seeding the report
|
|
542
|
+
// bar on open). Send `default: null` (or an empty default) to clear it. Only
|
|
543
|
+
// the keys provided are changed, in the order name, label, level, default,
|
|
544
|
+
// so a refused name leaves the rest unwritten.
|
|
541
545
|
updateFilterGroup(reportId, groupId, changes, config = {}) {
|
|
542
546
|
return this._api.put(this._path(`${reportId}/filtergroups/${groupId}`), changes, {
|
|
543
547
|
key: 'filterGroup',
|
|
@@ -1710,36 +1714,6 @@ function getZonedNow(timezone) {
|
|
|
1710
1714
|
}
|
|
1711
1715
|
}
|
|
1712
1716
|
|
|
1713
|
-
// The assembled report structure returned by GET /api/reports/:id — composed
|
|
1714
|
-
// server-side from the normalized tables (reports → report_pages →
|
|
1715
|
-
// report_components + report_filters, plus report_filter_groups). All keys are
|
|
1716
|
-
// camelCase; component `config` is the standard format the renderer maps to
|
|
1717
|
-
// ECharts options / FsList config.
|
|
1718
|
-
// How each granularity reads in the group-by control and on an export cover
|
|
1719
|
-
// sheet. WHICH of them a given report offers comes from its ReportGroupBy;
|
|
1720
|
-
// this is only the naming. 'year' reads as "Annually".
|
|
1721
|
-
const GRANULARITY_OPTIONS = [
|
|
1722
|
-
{ value: 'day', name: 'Day' },
|
|
1723
|
-
{ value: 'week', name: 'Week' },
|
|
1724
|
-
{ value: 'month', name: 'Month' },
|
|
1725
|
-
{ value: 'quarter', name: 'Quarter' },
|
|
1726
|
-
{ value: 'year', name: 'Annually' },
|
|
1727
|
-
];
|
|
1728
|
-
var ReportNodeType;
|
|
1729
|
-
(function (ReportNodeType) {
|
|
1730
|
-
ReportNodeType["Folder"] = "folder";
|
|
1731
|
-
ReportNodeType["Report"] = "report";
|
|
1732
|
-
})(ReportNodeType || (ReportNodeType = {}));
|
|
1733
|
-
// Reports and folders are both soft-deletable. A deleted report keeps its pages
|
|
1734
|
-
// and components; a deleted folder keeps everything filed under it, which leaves
|
|
1735
|
-
// the listing with it and comes back with it. "Show Deleted" shows the whole
|
|
1736
|
-
// tree, live and deleted together, so any of it can be restored in place.
|
|
1737
|
-
var ReportState;
|
|
1738
|
-
(function (ReportState) {
|
|
1739
|
-
ReportState["Active"] = "active";
|
|
1740
|
-
ReportState["Deleted"] = "deleted";
|
|
1741
|
-
})(ReportState || (ReportState = {}));
|
|
1742
|
-
|
|
1743
1717
|
// The most leading columns a list can keep in view while it scrolls sideways.
|
|
1744
1718
|
// Mirrors the server's ListConfig::MAX_PINNED_COLUMNS, which refuses more.
|
|
1745
1719
|
const MAX_PINNED_COLUMNS = 3;
|
|
@@ -1858,6 +1832,36 @@ var ReportChartDateLabel;
|
|
|
1858
1832
|
ReportChartDateLabel["Numeric"] = "numeric";
|
|
1859
1833
|
})(ReportChartDateLabel || (ReportChartDateLabel = {}));
|
|
1860
1834
|
|
|
1835
|
+
// The assembled report structure returned by GET /api/reports/:id — composed
|
|
1836
|
+
// server-side from the normalized tables (reports → report_pages →
|
|
1837
|
+
// report_components + report_filters, plus report_filter_groups). All keys are
|
|
1838
|
+
// camelCase; component `config` is the standard format the renderer maps to
|
|
1839
|
+
// ECharts options / FsList config.
|
|
1840
|
+
// How each granularity reads in the group-by control and on an export cover
|
|
1841
|
+
// sheet. WHICH of them a given report offers comes from its ReportGroupBy;
|
|
1842
|
+
// this is only the naming. 'year' reads as "Annually".
|
|
1843
|
+
const GRANULARITY_OPTIONS = [
|
|
1844
|
+
{ value: 'day', name: 'Day' },
|
|
1845
|
+
{ value: 'week', name: 'Week' },
|
|
1846
|
+
{ value: 'month', name: 'Month' },
|
|
1847
|
+
{ value: 'quarter', name: 'Quarter' },
|
|
1848
|
+
{ value: 'year', name: 'Annually' },
|
|
1849
|
+
];
|
|
1850
|
+
var ReportNodeType;
|
|
1851
|
+
(function (ReportNodeType) {
|
|
1852
|
+
ReportNodeType["Folder"] = "folder";
|
|
1853
|
+
ReportNodeType["Report"] = "report";
|
|
1854
|
+
})(ReportNodeType || (ReportNodeType = {}));
|
|
1855
|
+
// Reports and folders are both soft-deletable. A deleted report keeps its pages
|
|
1856
|
+
// and components; a deleted folder keeps everything filed under it, which leaves
|
|
1857
|
+
// the listing with it and comes back with it. "Show Deleted" shows the whole
|
|
1858
|
+
// tree, live and deleted together, so any of it can be restored in place.
|
|
1859
|
+
var ReportState;
|
|
1860
|
+
(function (ReportState) {
|
|
1861
|
+
ReportState["Active"] = "active";
|
|
1862
|
+
ReportState["Deleted"] = "deleted";
|
|
1863
|
+
})(ReportState || (ReportState = {}));
|
|
1864
|
+
|
|
1861
1865
|
// A chart measure's named color. The names follow the report chart palette in
|
|
1862
1866
|
// order (REPORT_CHART_COLOR_BY_NAME), so blue is the first series' default
|
|
1863
1867
|
// color and gray the tenth's.
|
|
@@ -4600,7 +4604,7 @@ const GROUP_VALUE_KEYS = {
|
|
|
4600
4604
|
const VIEW_CONFIG_KEYS = ['v', 'pageId', 'groupBy', 'match', 'groups', 'tiles'];
|
|
4601
4605
|
const TILE_STATE_KEYS = ['sort', 'search', 'hidden', 'drill'];
|
|
4602
4606
|
const DAY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
4603
|
-
// A number as text: what a masked input
|
|
4607
|
+
// A number as text: what a masked input sends.
|
|
4604
4608
|
const NUMBER_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
4605
4609
|
const ID_KEY_PATTERN = /^[1-9]\d*$/;
|
|
4606
4610
|
// Whether a name is a relative range the server resolves.
|
|
@@ -4620,8 +4624,8 @@ function isCalendarDay(text) {
|
|
|
4620
4624
|
const date = new Date(Date.UTC(year, month - 1, day));
|
|
4621
4625
|
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
4622
4626
|
}
|
|
4623
|
-
// A number written as text (a
|
|
4624
|
-
//
|
|
4627
|
+
// A number written as text (a masked input's value), or null when the text
|
|
4628
|
+
// is not one finite number.
|
|
4625
4629
|
function numberFromText(text) {
|
|
4626
4630
|
const number = NUMBER_PATTERN.test(text) ? Number(text) : NaN;
|
|
4627
4631
|
return Number.isFinite(number) ? number : null;
|
|
@@ -4642,7 +4646,7 @@ function groupValueFromArray(value, type) {
|
|
|
4642
4646
|
return keywordValue(value);
|
|
4643
4647
|
}
|
|
4644
4648
|
}
|
|
4645
|
-
// A group value read back (a saved view
|
|
4649
|
+
// A group value read back (a saved view) for a group of `type`: what
|
|
4646
4650
|
// groupValueFromArray gives, or null when it would refuse it.
|
|
4647
4651
|
function groupValueFromStored(value, type) {
|
|
4648
4652
|
if (!isPlainObject(value)) {
|
|
@@ -4680,7 +4684,7 @@ function viewConfigFromArray(input) {
|
|
|
4680
4684
|
}
|
|
4681
4685
|
return config;
|
|
4682
4686
|
}
|
|
4683
|
-
// A group's value as a view
|
|
4687
|
+
// A group's value as a view stores it, read for the keys its
|
|
4684
4688
|
// group's type owns, or null when it holds nothing. A date range that still
|
|
4685
4689
|
// carries its relative preset is stored as that preset whatever today is: the
|
|
4686
4690
|
// viewer did not edit it, so a view saved after midnight with the page still
|
|
@@ -4759,44 +4763,6 @@ function cleanDrillPath(path) {
|
|
|
4759
4763
|
function sameDrillPath(a, b) {
|
|
4760
4764
|
return drillKey(a) === drillKey(b);
|
|
4761
4765
|
}
|
|
4762
|
-
// What a link must carry for the screen (`current`, a snapshot) to open on
|
|
4763
|
-
// `base`: only what differs from it. A group `base` does not hold is read
|
|
4764
|
-
// against `authored`, as viewDiffers reads it. A key that went back to its
|
|
4765
|
-
// authored state where `base` holds another is written as that state where
|
|
4766
|
-
// a link can say it: a group's value (an empty value clears it) and a list's
|
|
4767
|
-
// sort (its authored first term, `authoredSort`). The match back to 'all',
|
|
4768
|
-
// the authored grain, an empty search, no hidden columns, a chart's top and
|
|
4769
|
-
// a list with no authored sort cannot be said on top of a base, so the
|
|
4770
|
-
// result is null and the caller writes the full diff from the authored
|
|
4771
|
-
// defaults instead, which can always be said. The page is never part of it
|
|
4772
|
-
// (the host writes its own key).
|
|
4773
|
-
function viewConfigDiff(base, current, authored, authoredSort) {
|
|
4774
|
-
const diff = { v: REPORT_VIEW_CONFIG_VERSION };
|
|
4775
|
-
if ((base.match ?? 'all') !== (current.match ?? 'all')) {
|
|
4776
|
-
if (current.match !== 'any') {
|
|
4777
|
-
return null;
|
|
4778
|
-
}
|
|
4779
|
-
diff.match = 'any';
|
|
4780
|
-
}
|
|
4781
|
-
if ((base.groupBy ?? null) !== (current.groupBy ?? null)) {
|
|
4782
|
-
if (!current.groupBy) {
|
|
4783
|
-
return null;
|
|
4784
|
-
}
|
|
4785
|
-
diff.groupBy = current.groupBy;
|
|
4786
|
-
}
|
|
4787
|
-
const groups = changedGroups(base, current, authored);
|
|
4788
|
-
if (Object.keys(groups).length) {
|
|
4789
|
-
diff.groups = groups;
|
|
4790
|
-
}
|
|
4791
|
-
const tiles = changedTiles(base.tiles ?? {}, current.tiles ?? {}, authoredSort);
|
|
4792
|
-
if (!tiles) {
|
|
4793
|
-
return null;
|
|
4794
|
-
}
|
|
4795
|
-
if (Object.keys(tiles).length) {
|
|
4796
|
-
diff.tiles = tiles;
|
|
4797
|
-
}
|
|
4798
|
-
return diff;
|
|
4799
|
-
}
|
|
4800
4766
|
// A view's config to save: the screen (`screen`, a snapshot), with the keys
|
|
4801
4767
|
// this viewer does not know kept from the stored document (`stored`), at the
|
|
4802
4768
|
// top and in each tile. They are never read here, and saving must not lose
|
|
@@ -4843,10 +4809,6 @@ function viewDroppedMessage(count, canChange = true) {
|
|
|
4843
4809
|
: `${count} settings in this view no longer apply, because the report changed.`;
|
|
4844
4810
|
return canChange ? `${note} Save changes to update the view.` : note;
|
|
4845
4811
|
}
|
|
4846
|
-
// Shown when a link names a view the viewer cannot open (deleted, or another
|
|
4847
|
-
// person's own view): the link's other settings apply on the report's
|
|
4848
|
-
// defaults.
|
|
4849
|
-
const VIEW_NOT_AVAILABLE_MESSAGE = 'The view in this link is not available.';
|
|
4850
4812
|
// The open view as a chat turn names it: its id, and whether the screen
|
|
4851
4813
|
// differs from it. Both are left out while no view is open.
|
|
4852
4814
|
function chatViewFields(openView, viewChanged) {
|
|
@@ -4864,66 +4826,6 @@ function tilesDiffer(base, current) {
|
|
|
4864
4826
|
const ids = new Set([...Object.keys(base), ...Object.keys(current)].map(Number));
|
|
4865
4827
|
return [...ids].some((componentId) => tileStateKey(base[componentId]) !== tileStateKey(current[componentId]));
|
|
4866
4828
|
}
|
|
4867
|
-
// The groups whose value on screen differs from base's (or, for a group base
|
|
4868
|
-
// does not hold, from the authored one), each with the screen's value; null
|
|
4869
|
-
// for one that holds nothing.
|
|
4870
|
-
function changedGroups(base, current, authored) {
|
|
4871
|
-
return Object.fromEntries(Object.entries(current.groups ?? {})
|
|
4872
|
-
.filter(([groupId, value]) => {
|
|
4873
|
-
const kept = base.groups && Object.prototype.hasOwnProperty.call(base.groups, groupId)
|
|
4874
|
-
? base.groups[Number(groupId)]
|
|
4875
|
-
: authored?.groups?.[Number(groupId)];
|
|
4876
|
-
return storedValueKey(kept) !== storedValueKey(value);
|
|
4877
|
-
})
|
|
4878
|
-
.map(([groupId, value]) => [groupId, value ?? null]));
|
|
4879
|
-
}
|
|
4880
|
-
// Each tile's keys that differ from base's, or null when one of them went
|
|
4881
|
-
// back to a state a link cannot say (see viewConfigDiff).
|
|
4882
|
-
function changedTiles(base, current, authoredSort) {
|
|
4883
|
-
const tiles = {};
|
|
4884
|
-
const ids = new Set([...Object.keys(base), ...Object.keys(current)].map(Number));
|
|
4885
|
-
for (const componentId of ids) {
|
|
4886
|
-
const tile = changedTile(base[componentId] ?? {}, current[componentId] ?? {}, () => authoredSort(componentId));
|
|
4887
|
-
if (!tile) {
|
|
4888
|
-
return null;
|
|
4889
|
-
}
|
|
4890
|
-
if (Object.keys(tile).length) {
|
|
4891
|
-
tiles[componentId] = tile;
|
|
4892
|
-
}
|
|
4893
|
-
}
|
|
4894
|
-
return tiles;
|
|
4895
|
-
}
|
|
4896
|
-
// One tile's keys that differ from base's; null when one went back to a
|
|
4897
|
-
// state a link cannot say.
|
|
4898
|
-
function changedTile(base, current, authoredSort) {
|
|
4899
|
-
const tile = {};
|
|
4900
|
-
if (sortKey(base.sort) !== sortKey(current.sort)) {
|
|
4901
|
-
const sort = current.sort?.column ? current.sort : authoredSort();
|
|
4902
|
-
if (!sort) {
|
|
4903
|
-
return null;
|
|
4904
|
-
}
|
|
4905
|
-
tile.sort = { ...sort };
|
|
4906
|
-
}
|
|
4907
|
-
if ((base.search ?? '') !== (current.search ?? '')) {
|
|
4908
|
-
if (!current.search) {
|
|
4909
|
-
return null;
|
|
4910
|
-
}
|
|
4911
|
-
tile.search = current.search;
|
|
4912
|
-
}
|
|
4913
|
-
if (sortedNames(base.hidden) !== sortedNames(current.hidden)) {
|
|
4914
|
-
if (!current.hidden?.length) {
|
|
4915
|
-
return null;
|
|
4916
|
-
}
|
|
4917
|
-
tile.hidden = [...current.hidden];
|
|
4918
|
-
}
|
|
4919
|
-
if (drillKey(base.drill) !== drillKey(current.drill)) {
|
|
4920
|
-
if (!current.drill?.length) {
|
|
4921
|
-
return null;
|
|
4922
|
-
}
|
|
4923
|
-
tile.drill = current.drill.map((step) => ({ ...step }));
|
|
4924
|
-
}
|
|
4925
|
-
return tile;
|
|
4926
|
-
}
|
|
4927
4829
|
// How many keys of a stored tile state the tile, as it applied, lost: a
|
|
4928
4830
|
// sort, a search or a drill path it no longer has, and hidden columns it
|
|
4929
4831
|
// kept fewer of.
|
|
@@ -4938,14 +4840,6 @@ function tileKeysLost(stored, applied) {
|
|
|
4938
4840
|
(stored.drill?.length ?? 0) > 0 && !applied?.drill?.length,
|
|
4939
4841
|
].filter(Boolean).length;
|
|
4940
4842
|
}
|
|
4941
|
-
// A list sort as text; no direction reads as ascending, the way a list
|
|
4942
|
-
// applies it.
|
|
4943
|
-
function sortKey(sort) {
|
|
4944
|
-
return sort?.column ? JSON.stringify([sort.column, sort.direction ?? 'asc']) : '';
|
|
4945
|
-
}
|
|
4946
|
-
function sortedNames(names) {
|
|
4947
|
-
return JSON.stringify([...(names ?? [])].sort());
|
|
4948
|
-
}
|
|
4949
4843
|
// One text for a stored group value, the same for every way of writing the
|
|
4950
4844
|
// same filter: picks in any order, text trimmed, and an empty value read as
|
|
4951
4845
|
// none.
|
|
@@ -5546,112 +5440,33 @@ function relativeLabel(relative) {
|
|
|
5546
5440
|
// query values back into per-group FilterGroupValues. Shared by the report-bar
|
|
5547
5441
|
// fs-filter and the per-component fs-filter so both render through the same
|
|
5548
5442
|
// library and write into ReportFilterStateService identically. Items are named
|
|
5549
|
-
// by the group's
|
|
5550
|
-
// the group
|
|
5443
|
+
// by the group's id (groupItemName), so the query keys map straight back to
|
|
5444
|
+
// the group. A report never puts these keys in the browser URL: fs-filter's
|
|
5445
|
+
// own query-param syncing is off in every report bar.
|
|
5551
5446
|
// The query key for the report's group-by control. Fixed rather than derived
|
|
5552
5447
|
// from an id because a report has exactly one — and it is NOT a filter group,
|
|
5553
5448
|
// so it is read straight off the change query instead of through
|
|
5554
|
-
// groupValuesFromQuery.
|
|
5555
|
-
//
|
|
5556
|
-
// request's `state.frequency`.
|
|
5449
|
+
// groupValuesFromQuery. The grain travels to the server as the data request's
|
|
5450
|
+
// `state.frequency`.
|
|
5557
5451
|
const GROUP_BY_ITEM = 'groupBy';
|
|
5558
|
-
// The
|
|
5559
|
-
|
|
5560
|
-
// not take one, and report-url-state.ts imports this file.
|
|
5561
|
-
// The shared view the state is a diff from, and the open personal view.
|
|
5562
|
-
const VIEW_PARAM = 'rv';
|
|
5563
|
-
const PERSONAL_VIEW_PARAM = 'rpv';
|
|
5564
|
-
// The link's format.
|
|
5565
|
-
const FORMAT_PARAM = 'rf';
|
|
5566
|
-
// The host's page key (FsAiReportComponent.pageQueryParam), as most hosts
|
|
5567
|
-
// name it.
|
|
5568
|
-
const PAGE_PARAM = 'page';
|
|
5569
|
-
// The word before a group's id in the key of a group whose label gives no
|
|
5570
|
-
// key it can use (filter12).
|
|
5571
|
-
const FALLBACK_KEY_WORD = 'filter';
|
|
5452
|
+
// The word before a group's id in the name of its fs-filter item (filter12).
|
|
5453
|
+
const ITEM_NAME_WORD = 'filter';
|
|
5572
5454
|
// The query key of one end of an fs-filter range item: the item's name and
|
|
5573
5455
|
// the end with its first letter upper-cased (fs-filter's getRangeName), so
|
|
5574
|
-
//
|
|
5456
|
+
// filter12 gives filter12Min and filter12From.
|
|
5575
5457
|
function itemRangeName(name, end) {
|
|
5576
5458
|
return `${name}${end.charAt(0).toUpperCase()}${end.slice(1)}`;
|
|
5577
5459
|
}
|
|
5578
|
-
//
|
|
5579
|
-
//
|
|
5580
|
-
//
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
// id has it or one of its forms, another link key or bar item uses it, or it
|
|
5584
|
-
// is shaped like a tile's id key, such as q4) is the key and the group's id
|
|
5585
|
-
// (region12). A label with no letter or digit, or a clash even then, keys on
|
|
5586
|
-
// filter<id>. `groups` is every group of the report, so every bar and the
|
|
5587
|
-
// link name a group alike.
|
|
5588
|
-
function groupParamKeys(groups) {
|
|
5589
|
-
// The URL's other keys and the bar's other items.
|
|
5590
|
-
const taken = new Set([
|
|
5591
|
-
PAGE_PARAM, GROUP_BY_ITEM, MATCH_ITEM, LIST_SEARCH_ITEM, VIEW_PARAM, PERSONAL_VIEW_PARAM, FORMAT_PARAM,
|
|
5592
|
-
]);
|
|
5593
|
-
const free = (candidate) => !!candidate
|
|
5594
|
-
&& keyForms(candidate).every((form) => !taken.has(form) && !ID_KEY.test(form));
|
|
5595
|
-
const keys = new Map();
|
|
5596
|
-
[...groups]
|
|
5597
|
-
.sort((a, b) => a.id - b.id)
|
|
5598
|
-
.forEach((group) => {
|
|
5599
|
-
const wanted = labelKey(groupLabel(group));
|
|
5600
|
-
const key = [wanted, wanted && `${wanted}${group.id}`].find(free) ?? fallbackKey(group.id, free);
|
|
5601
|
-
keyForms(key).forEach((form) => taken.add(form));
|
|
5602
|
-
keys.set(group.id, key);
|
|
5603
|
-
});
|
|
5604
|
-
return keys;
|
|
5605
|
-
}
|
|
5606
|
-
// filter<id>, or, in the rare report where another group's label already
|
|
5607
|
-
// gives that, filter<id> and a count.
|
|
5608
|
-
function fallbackKey(groupId, free) {
|
|
5609
|
-
const base = `${FALLBACK_KEY_WORD}${groupId}`;
|
|
5610
|
-
let key = base;
|
|
5611
|
-
for (let count = 2; !free(key); count++) {
|
|
5612
|
-
key = `${base}N${count}`;
|
|
5613
|
-
}
|
|
5614
|
-
return key;
|
|
5615
|
-
}
|
|
5616
|
-
// A group's key among `groups` (every group of the report). A group that is
|
|
5617
|
-
// not among them is keyed as one more of them.
|
|
5618
|
-
function groupParamKey(group, groups) {
|
|
5619
|
-
return groupParamKeys(groups.some((candidate) => candidate.id === group.id) ? groups : [...groups, group])
|
|
5620
|
-
.get(group.id);
|
|
5621
|
-
}
|
|
5622
|
-
// The name of a group's fs-filter item: its key among the report's groups.
|
|
5623
|
-
function groupItemName(group, report) {
|
|
5624
|
-
return groupParamKey(group, report?.filterGroups ?? []);
|
|
5460
|
+
// The name of a group's fs-filter item: 'filter' and the group's id. The id
|
|
5461
|
+
// never changes, so neither a new label nor a new name moves a bar's keys,
|
|
5462
|
+
// and no item name can take another item's (groupBy, match, listSearch).
|
|
5463
|
+
function groupItemName(group) {
|
|
5464
|
+
return `${ITEM_NAME_WORD}${group.id}`;
|
|
5625
5465
|
}
|
|
5626
5466
|
// The label a group's chip shows: its own, else its first member's column.
|
|
5627
5467
|
function groupLabel(group) {
|
|
5628
5468
|
return group.label || group.filters?.[0]?.filterColumn || '';
|
|
5629
5469
|
}
|
|
5630
|
-
// A letter and then digits: the shape of the URL's tile keys (s31, q31,
|
|
5631
|
-
// h31, d44), kept off the groups so a group key never reads as one.
|
|
5632
|
-
const ID_KEY = /^[a-z]\d+$/;
|
|
5633
|
-
const RANGE_ENDS = ['min', 'max', 'from', 'to'];
|
|
5634
|
-
// Every key a group's item or link may write under `key`: the key, its
|
|
5635
|
-
// exclude key (excludeRegion) and an fs-filter range's own keys.
|
|
5636
|
-
function keyForms(key) {
|
|
5637
|
-
return [key, getExcludeName(key), ...RANGE_ENDS.map((end) => itemRangeName(key, end))];
|
|
5638
|
-
}
|
|
5639
|
-
// A label as a camelCase key: accents dropped, words split on anything that
|
|
5640
|
-
// is not a letter or digit (and where a lower-case letter meets an upper-case
|
|
5641
|
-
// one), the first word lower-case, every later one capitalized.
|
|
5642
|
-
function labelKey(label) {
|
|
5643
|
-
return label
|
|
5644
|
-
.normalize('NFD')
|
|
5645
|
-
.replace(/\p{M}/gu, '')
|
|
5646
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
5647
|
-
.split(/[^A-Za-z0-9]+/)
|
|
5648
|
-
.filter((word) => word !== '')
|
|
5649
|
-
.map((word, index) => {
|
|
5650
|
-
const lower = word.toLowerCase();
|
|
5651
|
-
return index ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower;
|
|
5652
|
-
})
|
|
5653
|
-
.join('');
|
|
5654
|
-
}
|
|
5655
5470
|
// A select group's distinct options, as { name, value } pairs. The server sends
|
|
5656
5471
|
// { value, label }: the value is what the filter matches on (an entity's stable
|
|
5657
5472
|
// id) and the label is what the viewer reads — so a renamed entity keeps its id
|
|
@@ -5713,7 +5528,7 @@ const OPTIONS_FAILED_NOTE = 'Could not load the list.';
|
|
|
5713
5528
|
// columns), and a number range is a min/max control only when it lists
|
|
5714
5529
|
// numberRange.
|
|
5715
5530
|
function filterItemForGroup(group, options, report, initial, features = []) {
|
|
5716
|
-
const name = groupItemName(group
|
|
5531
|
+
const name = groupItemName(group);
|
|
5717
5532
|
const label = groupLabel(group);
|
|
5718
5533
|
switch (group.type) {
|
|
5719
5534
|
case 'dateRange':
|
|
@@ -5753,7 +5568,7 @@ function selectFilterItem(group, label, options, report, initial, exclude) {
|
|
|
5753
5568
|
const picks = excluding ? initial.excludeValues : initial?.values;
|
|
5754
5569
|
const options$ = options.options(group, report);
|
|
5755
5570
|
return {
|
|
5756
|
-
name: groupItemName(group
|
|
5571
|
+
name: groupItemName(group),
|
|
5757
5572
|
type: ItemType.AutoCompleteChips,
|
|
5758
5573
|
label,
|
|
5759
5574
|
fetchOnFocus: true,
|
|
@@ -5780,7 +5595,7 @@ function optionsNote(options, groupId) {
|
|
|
5780
5595
|
// 'Amount: 0 to 10'): the ends are read off the item's query the way the
|
|
5781
5596
|
// bar's change reads them, and worded in the report's locale.
|
|
5782
5597
|
function numberRangeFilterItem(group, label, report, initial) {
|
|
5783
|
-
const name = groupItemName(group
|
|
5598
|
+
const name = groupItemName(group);
|
|
5784
5599
|
const min = initial?.min ?? undefined;
|
|
5785
5600
|
const max = initial?.max ?? undefined;
|
|
5786
5601
|
const locale = resolveFormatContext(report ?? {}).locale;
|
|
@@ -5827,7 +5642,6 @@ function hasPageGroup(report) {
|
|
|
5827
5642
|
}
|
|
5828
5643
|
// The query key of the report bar's Match control. Not a filter group: it
|
|
5829
5644
|
// says how the groups combine, so it is read straight off the change query.
|
|
5830
|
-
// It is also the report URL's key for the match (match=any).
|
|
5831
5645
|
const MATCH_ITEM = 'match';
|
|
5832
5646
|
const MATCH_OPTIONS = [
|
|
5833
5647
|
{ name: 'All filters', value: 'all' },
|
|
@@ -5862,7 +5676,7 @@ function matchFromQuery(query) {
|
|
|
5862
5676
|
// the change query and travels as the list's own `state.search`.
|
|
5863
5677
|
const LIST_SEARCH_ITEM = 'listSearch';
|
|
5864
5678
|
// The longest search the server applies (ReportQueryRunner::MAX_SEARCH_LENGTH).
|
|
5865
|
-
// The viewer cuts the text to it too, so the
|
|
5679
|
+
// The viewer cuts the text to it too, so the state, the request and the export
|
|
5866
5680
|
// title all carry the text that was actually searched.
|
|
5867
5681
|
const LIST_SEARCH_MAX_LENGTH = 100;
|
|
5868
5682
|
// The fs-filter config item for a list's search box, seeded with the text the
|
|
@@ -5938,10 +5752,10 @@ function groupBySignature(report) {
|
|
|
5938
5752
|
}
|
|
5939
5753
|
// Read an fs-filter change query into per-group values. Returns one entry per
|
|
5940
5754
|
// group with the value to store (or null to clear) — callers feed these to
|
|
5941
|
-
// ReportFilterStateService.setValue.
|
|
5942
|
-
function groupValuesFromQuery(query, groups
|
|
5755
|
+
// ReportFilterStateService.setValue.
|
|
5756
|
+
function groupValuesFromQuery(query, groups) {
|
|
5943
5757
|
return groups.map((group) => {
|
|
5944
|
-
const name = groupItemName(group
|
|
5758
|
+
const name = groupItemName(group);
|
|
5945
5759
|
switch (group.type) {
|
|
5946
5760
|
case 'dateRange':
|
|
5947
5761
|
return { groupId: group.id, value: dateRangeFromQuery(query, name) };
|
|
@@ -5956,7 +5770,7 @@ function groupValuesFromQuery(query, groups, report) {
|
|
|
5956
5770
|
}
|
|
5957
5771
|
});
|
|
5958
5772
|
}
|
|
5959
|
-
// A date range's days (<
|
|
5773
|
+
// A date range's days (<name>From, <name>To).
|
|
5960
5774
|
function dateRangeFromQuery(query, name) {
|
|
5961
5775
|
const start = query[itemRangeName(name, 'from')];
|
|
5962
5776
|
const end = query[itemRangeName(name, 'to')];
|
|
@@ -5965,8 +5779,8 @@ function dateRangeFromQuery(query, name) {
|
|
|
5965
5779
|
}
|
|
5966
5780
|
return { start: start || null, end: end || null };
|
|
5967
5781
|
}
|
|
5968
|
-
// A select's picks: kept (<
|
|
5969
|
-
// (exclude<
|
|
5782
|
+
// A select's picks: kept (<name>), or left out while its exclude row is on
|
|
5783
|
+
// (exclude<Name>).
|
|
5970
5784
|
function selectFromQuery(query, name) {
|
|
5971
5785
|
const excludeValues = selectQueryValues(query[getExcludeName(name)]);
|
|
5972
5786
|
if (excludeValues.length) {
|
|
@@ -5975,7 +5789,7 @@ function selectFromQuery(query, name) {
|
|
|
5975
5789
|
const values = selectQueryValues(query[name]);
|
|
5976
5790
|
return values.length ? { values } : null;
|
|
5977
5791
|
}
|
|
5978
|
-
// A number range's ends (<
|
|
5792
|
+
// A number range's ends (<name>Min, <name>Max). An end left empty is open.
|
|
5979
5793
|
function numberRangeFromQuery(query, name) {
|
|
5980
5794
|
const min = rangeBound(query[itemRangeName(name, 'min')]);
|
|
5981
5795
|
const max = rangeBound(query[itemRangeName(name, 'max')]);
|
|
@@ -6028,616 +5842,6 @@ function selectQueryValues(raw) {
|
|
|
6028
5842
|
return String(raw).split(',');
|
|
6029
5843
|
}
|
|
6030
5844
|
|
|
6031
|
-
// The prefix of a list's sort key.
|
|
6032
|
-
const LIST_SORT_PARAM_PREFIX = 's';
|
|
6033
|
-
// The prefix of a list's search key.
|
|
6034
|
-
const LIST_SEARCH_PARAM_PREFIX = 'q';
|
|
6035
|
-
// The prefix of a list's hidden-columns key.
|
|
6036
|
-
const LIST_HIDDEN_PARAM_PREFIX = 'h';
|
|
6037
|
-
// The prefix of a chart's drill-path key.
|
|
6038
|
-
const DRILL_PARAM_PREFIX = 'd';
|
|
6039
|
-
// The match's key: the report bar's Match item's own name.
|
|
6040
|
-
const MATCH_PARAM = MATCH_ITEM;
|
|
6041
|
-
// The format a link this viewer writes carries.
|
|
6042
|
-
const URL_FORMAT = 2;
|
|
6043
|
-
// Between a drill step's level and its value.
|
|
6044
|
-
const DRILL_STEP_SEPARATOR = ':';
|
|
6045
|
-
// Between a select's picks, and between a pick's value and its name
|
|
6046
|
-
// (fs-filter's AutoCompleteChips syntax).
|
|
6047
|
-
const PICK_SEPARATOR = ',';
|
|
6048
|
-
const PICK_NAME_SEPARATOR = ':';
|
|
6049
|
-
// Before a ',', ':' or '\' that is part of a pick's value or name.
|
|
6050
|
-
const PICK_ESCAPE = '\\';
|
|
6051
|
-
// The most characters the codec's params may take in a link (the host's own
|
|
6052
|
-
// path and params are not counted).
|
|
6053
|
-
const REPORT_URL_MAX_LENGTH = 2000;
|
|
6054
|
-
// Shown once when a link left group values out (ReportUrlWrite.tooLong).
|
|
6055
|
-
const URL_TOO_LONG_MESSAGE = 'This filter is too long for a link; save a view to share it.';
|
|
6056
|
-
const MATCH_ANY = 'any';
|
|
6057
|
-
// The prefixes of the tile keys this codec owns, each followed by an id.
|
|
6058
|
-
const OWNED_PREFIXES = [LIST_SORT_PARAM_PREFIX, LIST_SEARCH_PARAM_PREFIX, LIST_HIDDEN_PARAM_PREFIX, DRILL_PARAM_PREFIX];
|
|
6059
|
-
// The keys this codec owns that carry no id and do not depend on the report.
|
|
6060
|
-
const OWNED_KEYS = [GROUP_BY_ITEM, MATCH_PARAM, VIEW_PARAM, PERSONAL_VIEW_PARAM, FORMAT_PARAM];
|
|
6061
|
-
// A key of one of the prefixes above, followed by its id.
|
|
6062
|
-
const ID_PARAM = new RegExp(`^([${OWNED_PREFIXES.join('')}])(\\d+)$`);
|
|
6063
|
-
// A key this codec owns whatever the report: a list's sort, search or hidden
|
|
6064
|
-
// columns; a chart's drill path; the grain, the match, the two views and the
|
|
6065
|
-
// format. The groups' keys are owned too (see the top of this file).
|
|
6066
|
-
const OWNED_PARAM = new RegExp(`^(?:[${OWNED_PREFIXES.join('')}]\\d+|${OWNED_KEYS.join('|')})$`);
|
|
6067
|
-
// A link's day: 'yyyy-MM-dd', alone or as the start of fs-filter's date
|
|
6068
|
-
// format ('T' and a time and offset after it).
|
|
6069
|
-
const LINK_DAY = /^(\d{4}-\d{2}-\d{2})(?:T.*)?$/;
|
|
6070
|
-
// The viewer-state params of a URL: the groups' values in their stored shape
|
|
6071
|
-
// (null for a group the link clears; read as paramsToViewConfig reads them),
|
|
6072
|
-
// the rest as raw strings. Keys this codec does not own are ignored.
|
|
6073
|
-
// `groups` is every filter group of the report.
|
|
6074
|
-
function readReportUrlParams(params, groups) {
|
|
6075
|
-
const listSorts = new Map();
|
|
6076
|
-
const listSearches = new Map();
|
|
6077
|
-
const listHidden = new Map();
|
|
6078
|
-
// Each prefix read here names the map its keys go to; a prefix with no
|
|
6079
|
-
// entry is never read as some other kind of state.
|
|
6080
|
-
const byPrefix = {
|
|
6081
|
-
[LIST_SORT_PARAM_PREFIX]: listSorts,
|
|
6082
|
-
[LIST_SEARCH_PARAM_PREFIX]: listSearches,
|
|
6083
|
-
[LIST_HIDDEN_PARAM_PREFIX]: listHidden,
|
|
6084
|
-
};
|
|
6085
|
-
const query = {};
|
|
6086
|
-
for (const key of params.keys) {
|
|
6087
|
-
query[key] = params.get(key) ?? '';
|
|
6088
|
-
const owned = ID_PARAM.exec(key);
|
|
6089
|
-
if (owned) {
|
|
6090
|
-
byPrefix[owned[1]]?.set(Number(owned[2]), params.get(key) ?? '');
|
|
6091
|
-
}
|
|
6092
|
-
}
|
|
6093
|
-
return {
|
|
6094
|
-
groups: readGroupParams(query, groups),
|
|
6095
|
-
listSorts, listSearches, listHidden,
|
|
6096
|
-
groupBy: params.get(GROUP_BY_ITEM),
|
|
6097
|
-
};
|
|
6098
|
-
}
|
|
6099
|
-
// The viewer state a link carries, as a view config: its group values (read
|
|
6100
|
-
// by each group's type, so a group the report does not have is skipped), the
|
|
6101
|
-
// grain, the match and each tile's sort, search, hidden columns and drill
|
|
6102
|
-
// path. A param that is unusable is left out. Whether a tile's component and
|
|
6103
|
-
// a list's columns exist is the caller's check. `groups` is every filter
|
|
6104
|
-
// group of the report.
|
|
6105
|
-
function paramsToViewConfig(params, groups) {
|
|
6106
|
-
const config = { v: REPORT_VIEW_CONFIG_VERSION };
|
|
6107
|
-
const groupBy = paramText(params[GROUP_BY_ITEM]);
|
|
6108
|
-
if (isChartGranularity(groupBy)) {
|
|
6109
|
-
config.groupBy = groupBy;
|
|
6110
|
-
}
|
|
6111
|
-
if (paramText(params[MATCH_PARAM]) === MATCH_ANY) {
|
|
6112
|
-
config.match = MATCH_ANY;
|
|
6113
|
-
}
|
|
6114
|
-
readGroupParams(params, groups).forEach((value, groupId) => {
|
|
6115
|
-
config.groups = { ...config.groups, [groupId]: value };
|
|
6116
|
-
});
|
|
6117
|
-
Object.keys(params).forEach((key) => {
|
|
6118
|
-
const owned = ID_PARAM.exec(key);
|
|
6119
|
-
const raw = paramText(params[key]);
|
|
6120
|
-
if (owned && raw !== null) {
|
|
6121
|
-
readTileParam(config, owned[1], Number(owned[2]), raw);
|
|
6122
|
-
}
|
|
6123
|
-
});
|
|
6124
|
-
return config;
|
|
6125
|
-
}
|
|
6126
|
-
// The params that make a link carry `config`, the views it names, and rf=2.
|
|
6127
|
-
// Each group is written under its keys among `groups` (every filter group of
|
|
6128
|
-
// the report); a group the report no longer has is left out. `name` names a
|
|
6129
|
-
// select's values. Past REPORT_URL_MAX_LENGTH the longest group values are
|
|
6130
|
-
// left out (those groups then open at the link's base) and tooLong is set.
|
|
6131
|
-
function viewConfigToParams(config, groups, link = {}, name = () => null) {
|
|
6132
|
-
const params = {};
|
|
6133
|
-
const keys = groupParamKeys(groups);
|
|
6134
|
-
const types = new Map(groups.map((group) => [group.id, group.type]));
|
|
6135
|
-
const groupParams = new Set();
|
|
6136
|
-
if (config.groupBy) {
|
|
6137
|
-
params[GROUP_BY_ITEM] = config.groupBy;
|
|
6138
|
-
}
|
|
6139
|
-
Object.entries(config.groups ?? {}).forEach(([id, value]) => {
|
|
6140
|
-
const groupId = Number(id);
|
|
6141
|
-
const key = keys.get(groupId);
|
|
6142
|
-
if (key) {
|
|
6143
|
-
const written = groupValueParams(types.get(groupId), key, value, (pick) => name(groupId, pick));
|
|
6144
|
-
Object.assign(params, written);
|
|
6145
|
-
Object.keys(written).forEach((param) => groupParams.add(param));
|
|
6146
|
-
}
|
|
6147
|
-
});
|
|
6148
|
-
Object.entries(config.tiles ?? {}).forEach(([componentId, tile]) => {
|
|
6149
|
-
Object.assign(params, tileParams(Number(componentId), tile));
|
|
6150
|
-
});
|
|
6151
|
-
if (config.match === MATCH_ANY) {
|
|
6152
|
-
params[MATCH_PARAM] = MATCH_ANY;
|
|
6153
|
-
}
|
|
6154
|
-
if (link.viewId) {
|
|
6155
|
-
params[VIEW_PARAM] = String(link.viewId);
|
|
6156
|
-
}
|
|
6157
|
-
if (link.personalViewId) {
|
|
6158
|
-
params[PERSONAL_VIEW_PARAM] = String(link.personalViewId);
|
|
6159
|
-
}
|
|
6160
|
-
return capReportParams(withFormat(params), groupParams);
|
|
6161
|
-
}
|
|
6162
|
-
// The views a link names, each a positive id or null.
|
|
6163
|
-
function readReportUrlLink(params) {
|
|
6164
|
-
return {
|
|
6165
|
-
viewId: positiveId(paramText(params[VIEW_PARAM])),
|
|
6166
|
-
personalViewId: positiveId(paramText(params[PERSONAL_VIEW_PARAM])),
|
|
6167
|
-
};
|
|
6168
|
-
}
|
|
6169
|
-
// Whether a link carries viewer state of its own, so it opens on that state
|
|
6170
|
-
// rather than on a default view: any owned key but the format marker and the
|
|
6171
|
-
// personal view, which is a label only. `groups` is every filter group of
|
|
6172
|
-
// the report.
|
|
6173
|
-
function hasReportUrlState(params, groups) {
|
|
6174
|
-
const groupKeys = ownedGroupKeys(groups);
|
|
6175
|
-
return Object.keys(params).some((key) => ID_PARAM.test(key) || groupKeys.has(key)
|
|
6176
|
-
|| key === GROUP_BY_ITEM || key === MATCH_PARAM || key === VIEW_PARAM);
|
|
6177
|
-
}
|
|
6178
|
-
// Every owned key `current` carries, set to null: under the router's 'merge'
|
|
6179
|
-
// handling, a write spread over these removes the keys it no longer carries.
|
|
6180
|
-
// Keys this codec does not own are left alone. `groups` is every filter
|
|
6181
|
-
// group of the report.
|
|
6182
|
-
function reportParamsToClear(current, groups) {
|
|
6183
|
-
const groupKeys = ownedGroupKeys(groups);
|
|
6184
|
-
const params = {};
|
|
6185
|
-
for (const key of current.keys) {
|
|
6186
|
-
if (OWNED_PARAM.test(key) || groupKeys.has(key)) {
|
|
6187
|
-
params[key] = null;
|
|
6188
|
-
}
|
|
6189
|
-
}
|
|
6190
|
-
return params;
|
|
6191
|
-
}
|
|
6192
|
-
// The names the select picks in `current` (the URL as it is) carry, so a
|
|
6193
|
-
// write can keep a name the viewer's lists have not named yet. Only a write
|
|
6194
|
-
// reads these: what the chips show comes from the lists.
|
|
6195
|
-
function urlValueNames(current, groups) {
|
|
6196
|
-
const keys = groupParamKeys(groups);
|
|
6197
|
-
const names = new Map();
|
|
6198
|
-
groups
|
|
6199
|
-
.filter((group) => group.type === 'select')
|
|
6200
|
-
.forEach((group) => {
|
|
6201
|
-
const key = keys.get(group.id);
|
|
6202
|
-
const picks = [current.get(key), current.get(getExcludeName(key))]
|
|
6203
|
-
.flatMap((raw) => (raw ? decodePickList(raw) : []))
|
|
6204
|
-
.filter((pick) => pick.name !== null);
|
|
6205
|
-
names.set(group.id, new Map(picks.map((pick) => [pick.value, pick.name])));
|
|
6206
|
-
});
|
|
6207
|
-
return (groupId, value) => names.get(groupId)?.get(String(value)) ?? null;
|
|
6208
|
-
}
|
|
6209
|
-
// A list's hidden column names as one param value. A name is a query's output
|
|
6210
|
-
// alias, which can hold a comma, so each is percent-encoded before the join.
|
|
6211
|
-
function encodeColumnList(names) {
|
|
6212
|
-
return names.map((name) => encodeURIComponent(name)).join(',');
|
|
6213
|
-
}
|
|
6214
|
-
// The column names of a hidden-columns param, in its order. Empty items are
|
|
6215
|
-
// dropped, and an item that does not decode (a hand-edited '%') is kept as
|
|
6216
|
-
// written. Whether a name is a column of the list is the caller's check.
|
|
6217
|
-
function decodeColumnList(raw) {
|
|
6218
|
-
return raw.split(',')
|
|
6219
|
-
.filter((item) => item !== '')
|
|
6220
|
-
.map(decodeItem);
|
|
6221
|
-
}
|
|
6222
|
-
// A group's value in its stored shape as the link's params under the group's
|
|
6223
|
-
// key (see the top of this file): days, then a select's values left out, then
|
|
6224
|
-
// its values kept, then a number range, then keyword text. null (a cleared
|
|
6225
|
-
// group), or a value that holds nothing, is the group's keys, empty. A
|
|
6226
|
-
// relative range with no days is not written at all: a link carries days, and
|
|
6227
|
-
// only the caller has the report's zone and clock to give them
|
|
6228
|
-
// (ReportViewService). `name` names a select's value.
|
|
6229
|
-
function groupValueParams(type, key, value, name = () => null) {
|
|
6230
|
-
if (!value) {
|
|
6231
|
-
return clearParams(type, key);
|
|
6232
|
-
}
|
|
6233
|
-
if (value.start || value.end) {
|
|
6234
|
-
return rangeParams(type, key, ['from', 'to'], [linkDayText(value.start), linkDayText(value.end)]);
|
|
6235
|
-
}
|
|
6236
|
-
if (value.excludeValues?.length) {
|
|
6237
|
-
return { [getExcludeName(key)]: encodePicks(value.excludeValues, name) };
|
|
6238
|
-
}
|
|
6239
|
-
if (value.values) {
|
|
6240
|
-
return { [key]: encodePicks(value.values, name) };
|
|
6241
|
-
}
|
|
6242
|
-
if (isNumberEnd(value.min) || isNumberEnd(value.max)) {
|
|
6243
|
-
return rangeParams(type, key, ['min', 'max'], [numberEndText(value.min), numberEndText(value.max)]);
|
|
6244
|
-
}
|
|
6245
|
-
if (value.relative) {
|
|
6246
|
-
return {};
|
|
6247
|
-
}
|
|
6248
|
-
return value.value ? { [key]: value.value } : clearParams(type, key);
|
|
6249
|
-
}
|
|
6250
|
-
function encodeSort(sort) {
|
|
6251
|
-
return `${sort.direction === 'desc' ? '-' : ''}${sort.column}`;
|
|
6252
|
-
}
|
|
6253
|
-
// A list sort read back from its URL param, or null when it names no column.
|
|
6254
|
-
// Whether the column is one the list can sort by is the caller's check.
|
|
6255
|
-
function decodeSort(raw) {
|
|
6256
|
-
const descending = raw.startsWith('-');
|
|
6257
|
-
const column = descending ? raw.slice(1) : raw;
|
|
6258
|
-
return column
|
|
6259
|
-
? { column, direction: descending ? 'desc' : 'asc' }
|
|
6260
|
-
: null;
|
|
6261
|
-
}
|
|
6262
|
-
// A drill path as its URL param. A step with no value writes its level only.
|
|
6263
|
-
function encodeDrillPath(path) {
|
|
6264
|
-
return path
|
|
6265
|
-
.map((step) => (step.value === null || step.value === undefined
|
|
6266
|
-
? encodeURIComponent(step.level)
|
|
6267
|
-
: `${encodeURIComponent(step.level)}${DRILL_STEP_SEPARATOR}${encodeURIComponent(String(step.value))}`))
|
|
6268
|
-
.join(',');
|
|
6269
|
-
}
|
|
6270
|
-
// A drill path read back from its URL param: its steps up to the first that
|
|
6271
|
-
// is not a time level (the steps before it are kept), at most DRILL_MAX_DEPTH
|
|
6272
|
-
// of them. Values come back as text. Whether the path fits the chart is the
|
|
6273
|
-
// server's check (it applies the longest valid prefix).
|
|
6274
|
-
function decodeDrillPath(raw) {
|
|
6275
|
-
const path = [];
|
|
6276
|
-
for (const item of raw.split(',')) {
|
|
6277
|
-
const step = decodeDrillStep(item);
|
|
6278
|
-
if (!step || path.length === DRILL_MAX_DEPTH) {
|
|
6279
|
-
break;
|
|
6280
|
-
}
|
|
6281
|
-
path.push(step);
|
|
6282
|
-
}
|
|
6283
|
-
return path;
|
|
6284
|
-
}
|
|
6285
|
-
// The query params that make the URL carry `snapshot`: every snapshot key is
|
|
6286
|
-
// set, and each owned key that `current` carries but the snapshot does not is
|
|
6287
|
-
// set to null, which removes it under the router's 'merge' handling. Keys
|
|
6288
|
-
// this codec does not own are left alone. Each group is written under its
|
|
6289
|
-
// keys among `groups` (every filter group of the report). A select's value
|
|
6290
|
-
// is named by `name`, else by the name `current` carries for it. A date range
|
|
6291
|
-
// is written as the days on screen, never as a preset name.
|
|
6292
|
-
function buildReportUrlParams(snapshot, current, groups, name = () => null) {
|
|
6293
|
-
const params = {};
|
|
6294
|
-
const keys = groupParamKeys(groups);
|
|
6295
|
-
const types = new Map(groups.map((group) => [group.id, group.type]));
|
|
6296
|
-
const currentName = urlValueNames(current, groups);
|
|
6297
|
-
snapshot.groups.forEach((value, groupId) => {
|
|
6298
|
-
const key = keys.get(groupId);
|
|
6299
|
-
if (key) {
|
|
6300
|
-
const type = types.get(groupId);
|
|
6301
|
-
const named = (pick) => name(groupId, pick) ?? currentName(groupId, pick);
|
|
6302
|
-
const days = value ? { ...value, preset: undefined } : value;
|
|
6303
|
-
Object.assign(params, groupValueParams(type, key, toStoredGroupValue(type, days), named));
|
|
6304
|
-
}
|
|
6305
|
-
});
|
|
6306
|
-
snapshot.listSorts.forEach((sort, componentId) => {
|
|
6307
|
-
params[`${LIST_SORT_PARAM_PREFIX}${componentId}`] = encodeSort(sort);
|
|
6308
|
-
});
|
|
6309
|
-
// The text as typed: the router percent-encodes it in the URL.
|
|
6310
|
-
snapshot.listSearches.forEach((search, componentId) => {
|
|
6311
|
-
params[`${LIST_SEARCH_PARAM_PREFIX}${componentId}`] = search;
|
|
6312
|
-
});
|
|
6313
|
-
snapshot.listHidden.forEach((names, componentId) => {
|
|
6314
|
-
params[`${LIST_HIDDEN_PARAM_PREFIX}${componentId}`] = encodeColumnList(names);
|
|
6315
|
-
});
|
|
6316
|
-
if (snapshot.frequency) {
|
|
6317
|
-
params[GROUP_BY_ITEM] = snapshot.frequency;
|
|
6318
|
-
}
|
|
6319
|
-
return { ...reportParamsToClear(current, groups), ...withFormat(params) };
|
|
6320
|
-
}
|
|
6321
|
-
// `params` with the format marker, when it carries anything.
|
|
6322
|
-
function withFormat(params) {
|
|
6323
|
-
return Object.keys(params).length ? { ...params, [FORMAT_PARAM]: String(URL_FORMAT) } : params;
|
|
6324
|
-
}
|
|
6325
|
-
// Each group's value a link carries, in its stored shape, by group id (null
|
|
6326
|
-
// for a group the link clears). A group with no usable param is left out.
|
|
6327
|
-
function readGroupParams(params, groups) {
|
|
6328
|
-
const keys = groupParamKeys(groups);
|
|
6329
|
-
const values = new Map();
|
|
6330
|
-
for (const group of groups) {
|
|
6331
|
-
const value = readGroupParam(params, group.type, keys.get(group.id));
|
|
6332
|
-
if (value !== undefined) {
|
|
6333
|
-
values.set(group.id, value);
|
|
6334
|
-
}
|
|
6335
|
-
}
|
|
6336
|
-
return values;
|
|
6337
|
-
}
|
|
6338
|
-
// One group's value in a link, read from the keys of its type. null is a
|
|
6339
|
-
// clear; undefined is no usable value.
|
|
6340
|
-
function readGroupParam(params, type, key) {
|
|
6341
|
-
switch (type) {
|
|
6342
|
-
case 'select':
|
|
6343
|
-
return readSelect(params, key);
|
|
6344
|
-
case 'dateRange':
|
|
6345
|
-
return readDateRange(params, key);
|
|
6346
|
-
case 'numberRange':
|
|
6347
|
-
return readNumberRange(params, key);
|
|
6348
|
-
case 'keyword':
|
|
6349
|
-
return readKeyword(params[key]);
|
|
6350
|
-
default:
|
|
6351
|
-
return undefined;
|
|
6352
|
-
}
|
|
6353
|
-
}
|
|
6354
|
-
// A select's values kept (<key>), else its values left out (exclude<Key>).
|
|
6355
|
-
function readSelect(params, key) {
|
|
6356
|
-
const kept = paramText(params[key]);
|
|
6357
|
-
if (kept !== null) {
|
|
6358
|
-
return kept === '' ? null : decodePicks(kept, 'values');
|
|
6359
|
-
}
|
|
6360
|
-
const excluded = paramText(params[getExcludeName(key)]);
|
|
6361
|
-
if (excluded !== null) {
|
|
6362
|
-
return excluded === '' ? null : decodePicks(excluded, 'excludeValues');
|
|
6363
|
-
}
|
|
6364
|
-
return undefined;
|
|
6365
|
-
}
|
|
6366
|
-
// A date range's days (<key>From, <key>To), each a real calendar day,
|
|
6367
|
-
// either one open but not both.
|
|
6368
|
-
function readDateRange(params, key) {
|
|
6369
|
-
const ends = readRangeEnds(params, key, ['from', 'to']);
|
|
6370
|
-
if (!ends) {
|
|
6371
|
-
return ends === null ? null : undefined;
|
|
6372
|
-
}
|
|
6373
|
-
const [start, end] = ends.map(linkDay);
|
|
6374
|
-
if (start === undefined || end === undefined) {
|
|
6375
|
-
return undefined;
|
|
6376
|
-
}
|
|
6377
|
-
return compactRange(start, end, 'start', 'end');
|
|
6378
|
-
}
|
|
6379
|
-
// A number range's ends (<key>Min, <key>Max), each a finite number, either
|
|
6380
|
-
// one open but not both, and min not above max.
|
|
6381
|
-
function readNumberRange(params, key) {
|
|
6382
|
-
const ends = readRangeEnds(params, key, ['min', 'max']);
|
|
6383
|
-
if (!ends) {
|
|
6384
|
-
return ends === null ? null : undefined;
|
|
6385
|
-
}
|
|
6386
|
-
const [min, max] = ends.map((end) => (end === '' ? null : numberFromText(end)));
|
|
6387
|
-
if ((ends[0] !== '' && min === null) || (ends[1] !== '' && max === null) || (min !== null && max !== null && min > max)) {
|
|
6388
|
-
return undefined;
|
|
6389
|
-
}
|
|
6390
|
-
return compactRange(min, max, 'min', 'max');
|
|
6391
|
-
}
|
|
6392
|
-
// A keyword's text. Blank text is unusable.
|
|
6393
|
-
function readKeyword(raw) {
|
|
6394
|
-
const text = paramText(raw);
|
|
6395
|
-
if (text === null) {
|
|
6396
|
-
return undefined;
|
|
6397
|
-
}
|
|
6398
|
-
if (text === '') {
|
|
6399
|
-
return null;
|
|
6400
|
-
}
|
|
6401
|
-
return text.trim() ? { value: text } : undefined;
|
|
6402
|
-
}
|
|
6403
|
-
// A range's two ends as written ('' for one the link leaves open):
|
|
6404
|
-
// undefined when the link carries neither key, null when every one it
|
|
6405
|
-
// carries is empty (a clear).
|
|
6406
|
-
function readRangeEnds(params, key, ends) {
|
|
6407
|
-
const [low, high] = ends.map((end) => paramText(params[itemRangeName(key, end)]));
|
|
6408
|
-
if (low === null && high === null) {
|
|
6409
|
-
return undefined;
|
|
6410
|
-
}
|
|
6411
|
-
return low || high ? [low ?? '', high ?? ''] : null;
|
|
6412
|
-
}
|
|
6413
|
-
// The params of a range's ends, the ones it sets only; with neither one
|
|
6414
|
-
// usable, the group's clear.
|
|
6415
|
-
function rangeParams(type, key, ends, texts) {
|
|
6416
|
-
const params = {};
|
|
6417
|
-
ends.forEach((end, index) => {
|
|
6418
|
-
if (texts[index]) {
|
|
6419
|
-
params[itemRangeName(key, end)] = texts[index];
|
|
6420
|
-
}
|
|
6421
|
-
});
|
|
6422
|
-
return Object.keys(params).length ? params : clearParams(type, key);
|
|
6423
|
-
}
|
|
6424
|
-
// The params of a cleared group: its keys, empty.
|
|
6425
|
-
function clearParams(type, key) {
|
|
6426
|
-
switch (type) {
|
|
6427
|
-
case 'dateRange':
|
|
6428
|
-
return { [itemRangeName(key, 'from')]: '', [itemRangeName(key, 'to')]: '' };
|
|
6429
|
-
case 'numberRange':
|
|
6430
|
-
return { [itemRangeName(key, 'min')]: '', [itemRangeName(key, 'max')]: '' };
|
|
6431
|
-
default:
|
|
6432
|
-
return { [key]: '' };
|
|
6433
|
-
}
|
|
6434
|
-
}
|
|
6435
|
-
// A stored day in fs-filter's date format: its local midnight with the
|
|
6436
|
-
// offset, '' when there is no real day.
|
|
6437
|
-
function linkDayText(bound) {
|
|
6438
|
-
const day = dateBoundString(bound);
|
|
6439
|
-
return day && isCalendarDay(day) ? iso8601(parseISO(day)) : '';
|
|
6440
|
-
}
|
|
6441
|
-
// The day of a link's date end: null when the end is open (''), undefined
|
|
6442
|
-
// when it is not a real calendar day.
|
|
6443
|
-
function linkDay(text) {
|
|
6444
|
-
if (text === '') {
|
|
6445
|
-
return null;
|
|
6446
|
-
}
|
|
6447
|
-
const day = LINK_DAY.exec(text)?.[1];
|
|
6448
|
-
return day && isCalendarDay(day) ? day : undefined;
|
|
6449
|
-
}
|
|
6450
|
-
// The keys a link carries the groups' values under, by each group's type.
|
|
6451
|
-
function ownedGroupKeys(groups) {
|
|
6452
|
-
const keys = groupParamKeys(groups);
|
|
6453
|
-
return new Set(groups.flatMap((group) => {
|
|
6454
|
-
const key = keys.get(group.id);
|
|
6455
|
-
switch (group.type) {
|
|
6456
|
-
case 'select':
|
|
6457
|
-
return [key, getExcludeName(key)];
|
|
6458
|
-
case 'dateRange':
|
|
6459
|
-
return [itemRangeName(key, 'from'), itemRangeName(key, 'to')];
|
|
6460
|
-
case 'numberRange':
|
|
6461
|
-
return [itemRangeName(key, 'min'), itemRangeName(key, 'max')];
|
|
6462
|
-
default:
|
|
6463
|
-
return [key];
|
|
6464
|
-
}
|
|
6465
|
-
}));
|
|
6466
|
-
}
|
|
6467
|
-
// One id-keyed param of a tile read into `config`.
|
|
6468
|
-
function readTileParam(config, prefix, id, raw) {
|
|
6469
|
-
switch (prefix) {
|
|
6470
|
-
case LIST_SORT_PARAM_PREFIX:
|
|
6471
|
-
setTileState(config, id, 'sort', decodeSort(raw));
|
|
6472
|
-
break;
|
|
6473
|
-
case LIST_SEARCH_PARAM_PREFIX:
|
|
6474
|
-
setTileState(config, id, 'search', raw || null);
|
|
6475
|
-
break;
|
|
6476
|
-
case LIST_HIDDEN_PARAM_PREFIX:
|
|
6477
|
-
setTileState(config, id, 'hidden', nonEmpty(decodeColumnList(raw)));
|
|
6478
|
-
break;
|
|
6479
|
-
default:
|
|
6480
|
-
setTileState(config, id, 'drill', nonEmpty(decodeDrillPath(raw)));
|
|
6481
|
-
}
|
|
6482
|
-
}
|
|
6483
|
-
// Sets one key of a tile's state, creating the tile, unless there is nothing
|
|
6484
|
-
// to set.
|
|
6485
|
-
function setTileState(config, componentId, key, value) {
|
|
6486
|
-
if (value === null) {
|
|
6487
|
-
return;
|
|
6488
|
-
}
|
|
6489
|
-
config.tiles = { ...config.tiles, [componentId]: { ...config.tiles?.[componentId], [key]: value } };
|
|
6490
|
-
}
|
|
6491
|
-
// The params of one tile's state.
|
|
6492
|
-
function tileParams(componentId, tile) {
|
|
6493
|
-
const params = {};
|
|
6494
|
-
if (tile.sort?.column) {
|
|
6495
|
-
params[`${LIST_SORT_PARAM_PREFIX}${componentId}`] = encodeSort(tile.sort);
|
|
6496
|
-
}
|
|
6497
|
-
// The text as typed: the router percent-encodes it in the URL.
|
|
6498
|
-
if (tile.search) {
|
|
6499
|
-
params[`${LIST_SEARCH_PARAM_PREFIX}${componentId}`] = tile.search;
|
|
6500
|
-
}
|
|
6501
|
-
if (tile.hidden?.length) {
|
|
6502
|
-
params[`${LIST_HIDDEN_PARAM_PREFIX}${componentId}`] = encodeColumnList(tile.hidden);
|
|
6503
|
-
}
|
|
6504
|
-
if (tile.drill?.length) {
|
|
6505
|
-
params[`${DRILL_PARAM_PREFIX}${componentId}`] = encodeDrillPath(tile.drill);
|
|
6506
|
-
}
|
|
6507
|
-
return params;
|
|
6508
|
-
}
|
|
6509
|
-
// `params` within REPORT_URL_MAX_LENGTH: past it, group values (the params
|
|
6510
|
-
// named in `groupParams`) are left out, longest first, until the rest fits.
|
|
6511
|
-
function capReportParams(params, groupParams) {
|
|
6512
|
-
if (queryLength(params) <= REPORT_URL_MAX_LENGTH) {
|
|
6513
|
-
return { params, tooLong: false };
|
|
6514
|
-
}
|
|
6515
|
-
const kept = { ...params };
|
|
6516
|
-
const groupKeys = Object.keys(kept)
|
|
6517
|
-
.filter((key) => groupParams.has(key))
|
|
6518
|
-
.sort((a, b) => queryText(kept[b]).length - queryText(kept[a]).length);
|
|
6519
|
-
for (const key of groupKeys) {
|
|
6520
|
-
if (queryLength(kept) <= REPORT_URL_MAX_LENGTH) {
|
|
6521
|
-
break;
|
|
6522
|
-
}
|
|
6523
|
-
delete kept[key];
|
|
6524
|
-
}
|
|
6525
|
-
return { params: kept, tooLong: true };
|
|
6526
|
-
}
|
|
6527
|
-
// The length of `params` as the router writes them in a query string.
|
|
6528
|
-
function queryLength(params) {
|
|
6529
|
-
return Object.entries(params)
|
|
6530
|
-
.map(([key, value]) => `${queryText(key)}=${queryText(value)}`)
|
|
6531
|
-
.join('&')
|
|
6532
|
-
.length;
|
|
6533
|
-
}
|
|
6534
|
-
// Text as the router writes it in a query string (its query encoding):
|
|
6535
|
-
// percent-encoded, with '@', ':', '$', ',' and ';' left as they are.
|
|
6536
|
-
function queryText(text) {
|
|
6537
|
-
return encodeURIComponent(text)
|
|
6538
|
-
.replace(/%40/g, '@')
|
|
6539
|
-
.replace(/%3A/gi, ':')
|
|
6540
|
-
.replace(/%24/g, '$')
|
|
6541
|
-
.replace(/%2C/gi, ',')
|
|
6542
|
-
.replace(/%3B/gi, ';');
|
|
6543
|
-
}
|
|
6544
|
-
// A param's text: a repeated key gives its first value.
|
|
6545
|
-
function paramText(value) {
|
|
6546
|
-
const first = Array.isArray(value) ? value[0] : value;
|
|
6547
|
-
return typeof first === 'string' || typeof first === 'number' ? String(first) : null;
|
|
6548
|
-
}
|
|
6549
|
-
function positiveId(raw) {
|
|
6550
|
-
return raw !== null && /^[1-9]\d*$/.test(raw) ? Number(raw) : null;
|
|
6551
|
-
}
|
|
6552
|
-
function nonEmpty(list) {
|
|
6553
|
-
return list.length ? list : null;
|
|
6554
|
-
}
|
|
6555
|
-
// An item of a list param, percent-decoded; one that does not decode (a
|
|
6556
|
-
// hand-edited '%') is kept as written.
|
|
6557
|
-
function decodeItem(item) {
|
|
6558
|
-
try {
|
|
6559
|
-
return decodeURIComponent(item);
|
|
6560
|
-
}
|
|
6561
|
-
catch {
|
|
6562
|
-
return item;
|
|
6563
|
-
}
|
|
6564
|
-
}
|
|
6565
|
-
// Select picks as one param value: value:name for each, the name left off
|
|
6566
|
-
// when `name` does not know it, blanks left out.
|
|
6567
|
-
function encodePicks(picks, name) {
|
|
6568
|
-
return picks
|
|
6569
|
-
.filter((pick) => typeof pick === 'string' || typeof pick === 'number' || typeof pick === 'boolean')
|
|
6570
|
-
.filter((pick) => String(pick) !== '')
|
|
6571
|
-
.map((pick) => {
|
|
6572
|
-
const named = name(pick);
|
|
6573
|
-
return named ? `${escapePick(String(pick))}${PICK_NAME_SEPARATOR}${escapePick(named)}` : escapePick(String(pick));
|
|
6574
|
-
})
|
|
6575
|
-
.join(PICK_SEPARATOR);
|
|
6576
|
-
}
|
|
6577
|
-
// A pick's value or name with each ',', ':' and '\' in it escaped.
|
|
6578
|
-
function escapePick(text) {
|
|
6579
|
-
return text.replace(/[\\,:]/g, (char) => `${PICK_ESCAPE}${char}`);
|
|
6580
|
-
}
|
|
6581
|
-
// The picks of a param written as encodePicks writes them, in order: each
|
|
6582
|
-
// value and its name (null when the pick has none).
|
|
6583
|
-
function decodePickList(raw) {
|
|
6584
|
-
const picks = [];
|
|
6585
|
-
let parts = [''];
|
|
6586
|
-
let escaped = false;
|
|
6587
|
-
const endPick = () => picks.push({ value: parts[0], name: parts.length > 1 ? parts[1] : null });
|
|
6588
|
-
for (const char of raw) {
|
|
6589
|
-
if (escaped) {
|
|
6590
|
-
parts[parts.length - 1] += char;
|
|
6591
|
-
escaped = false;
|
|
6592
|
-
}
|
|
6593
|
-
else if (char === PICK_ESCAPE) {
|
|
6594
|
-
escaped = true;
|
|
6595
|
-
}
|
|
6596
|
-
else if (char === PICK_NAME_SEPARATOR && parts.length === 1) {
|
|
6597
|
-
parts.push('');
|
|
6598
|
-
}
|
|
6599
|
-
else if (char === PICK_SEPARATOR) {
|
|
6600
|
-
endPick();
|
|
6601
|
-
parts = [''];
|
|
6602
|
-
}
|
|
6603
|
-
else {
|
|
6604
|
-
parts[parts.length - 1] += char;
|
|
6605
|
-
}
|
|
6606
|
-
}
|
|
6607
|
-
endPick();
|
|
6608
|
-
return picks;
|
|
6609
|
-
}
|
|
6610
|
-
// A select's values, kept or left out, read from its param, or undefined
|
|
6611
|
-
// when there are none or too many.
|
|
6612
|
-
function decodePicks(raw, as) {
|
|
6613
|
-
const picks = decodePickList(raw)
|
|
6614
|
-
.map((pick) => pick.value)
|
|
6615
|
-
.filter((value) => value !== '');
|
|
6616
|
-
return picks.length && picks.length <= GROUP_VALUE_MAX_PICKS ? { [as]: picks } : undefined;
|
|
6617
|
-
}
|
|
6618
|
-
// A range's set ends under their keys.
|
|
6619
|
-
function compactRange(low, high, lowKey, highKey) {
|
|
6620
|
-
return {
|
|
6621
|
-
...(low !== null ? { [lowKey]: low } : {}),
|
|
6622
|
-
...(high !== null ? { [highKey]: high } : {}),
|
|
6623
|
-
};
|
|
6624
|
-
}
|
|
6625
|
-
function isNumberEnd(end) {
|
|
6626
|
-
return typeof end === 'number' && Number.isFinite(end);
|
|
6627
|
-
}
|
|
6628
|
-
function numberEndText(end) {
|
|
6629
|
-
return isNumberEnd(end) ? String(end) : '';
|
|
6630
|
-
}
|
|
6631
|
-
// One 'level:value' step, or null when its level is not a time grain.
|
|
6632
|
-
function decodeDrillStep(item) {
|
|
6633
|
-
const at = item.indexOf(DRILL_STEP_SEPARATOR);
|
|
6634
|
-
const level = decodeItem(at < 0 ? item : item.slice(0, at));
|
|
6635
|
-
if (!isChartGranularity(level)) {
|
|
6636
|
-
return null;
|
|
6637
|
-
}
|
|
6638
|
-
return { level, value: at < 0 ? null : decodeItem(item.slice(at + 1)) };
|
|
6639
|
-
}
|
|
6640
|
-
|
|
6641
5845
|
// Whether a component is a chart on a time x-axis: the only component the
|
|
6642
5846
|
// report's group-by grain applies to. A deliberate mirror of the server's
|
|
6643
5847
|
// ReportQueryRunner::_isTimeSeriesChart, which is canonical and drops the
|
|
@@ -6976,12 +6180,6 @@ function sameList(a, b) {
|
|
|
6976
6180
|
return (a ?? []).length === (b ?? []).length
|
|
6977
6181
|
&& (a ?? []).every((value, index) => value === (b ?? [])[index]);
|
|
6978
6182
|
}
|
|
6979
|
-
// Whether two select pick lists hold the same picks in any order, a number
|
|
6980
|
-
// and its text being the same pick.
|
|
6981
|
-
function samePicks(a, b) {
|
|
6982
|
-
const picks = (list) => (list ?? []).map(String).sort();
|
|
6983
|
-
return sameList(picks(a), picks(b));
|
|
6984
|
-
}
|
|
6985
6183
|
// Whether two list sorts order the rows the same way. No direction reads as
|
|
6986
6184
|
// ascending, the way the list applies it.
|
|
6987
6185
|
function sameSort(a, b) {
|
|
@@ -7060,18 +6258,20 @@ function emptyTemporaryState() {
|
|
|
7060
6258
|
// resolves them into per-component member-filter values for each data request,
|
|
7061
6259
|
// and notifies the components whose groups changed so only they refetch.
|
|
7062
6260
|
//
|
|
7063
|
-
// It is
|
|
7064
|
-
// values, the group-by grain, list sorts, searches and hidden
|
|
7065
|
-
//
|
|
7066
|
-
//
|
|
6261
|
+
// It is the one place the viewer's state lives while the report is open: the
|
|
6262
|
+
// group values, the group-by grain, the match, list sorts, searches and hidden
|
|
6263
|
+
// columns, the page and each chart's drill path. None of it is ever written to
|
|
6264
|
+
// the browser URL. It outlives a page switch (the canvas changes the page, not
|
|
6265
|
+
// this service) and goes with the report view: a reload starts a new service,
|
|
6266
|
+
// which opens on the report's defaults or the viewer's default view.
|
|
6267
|
+
// changes() fires on every change of it.
|
|
7067
6268
|
//
|
|
7068
|
-
// The
|
|
7069
|
-
//
|
|
7070
|
-
//
|
|
7071
|
-
//
|
|
7072
|
-
// panel) is its own section, so no view or link ever carries it.
|
|
6269
|
+
// The durable part is one view document: snapshot() writes it, apply() opens
|
|
6270
|
+
// a view, and base and viewChanged say whether the screen still shows that
|
|
6271
|
+
// view. The temporary layer (a cross-filter, an open rows panel) is its own
|
|
6272
|
+
// section, so no view ever carries it.
|
|
7073
6273
|
//
|
|
7074
|
-
// Session-only, never in
|
|
6274
|
+
// Session-only, never in a view: per-filter enable/disable toggles from a
|
|
7075
6275
|
// component's filter menu (the `enabled` column is the authored default; a
|
|
7076
6276
|
// viewer's toggle shouldn't write to the report).
|
|
7077
6277
|
class ReportFilterStateService {
|
|
@@ -7144,7 +6344,7 @@ class ReportFilterStateService {
|
|
|
7144
6344
|
_cleared = new Set();
|
|
7145
6345
|
// The report's group-by control: a single report-wide bucket override for all
|
|
7146
6346
|
// time-series charts. Unset = each chart uses its authored granularity. Never
|
|
7147
|
-
// saved to the report (
|
|
6347
|
+
// saved to the report (a saved view keeps it), and carried in the data request's
|
|
7148
6348
|
// `state`, so it propagates through to the PDF/PowerPoint exports too. Named
|
|
7149
6349
|
// for the `frequency` wire key it travels under, not for the control's label.
|
|
7150
6350
|
_frequency = null;
|
|
@@ -7169,17 +6369,17 @@ class ReportFilterStateService {
|
|
|
7169
6369
|
// Emits the group id that changed; components listen filtered to their own
|
|
7170
6370
|
// group ids. null = everything changed (re-init).
|
|
7171
6371
|
_changed$ = new Subject();
|
|
7172
|
-
// Fires whenever anything the
|
|
7173
|
-
|
|
7174
|
-
// Counts the changes
|
|
6372
|
+
// Fires whenever anything in the viewer state may have changed.
|
|
6373
|
+
_stateChanged$ = new Subject();
|
|
6374
|
+
// Counts the changes changes() reports. The group values, the grain and
|
|
7175
6375
|
// the list state that base and viewChanged compare are the plain fields
|
|
7176
|
-
// above, which every setter reports through
|
|
6376
|
+
// above, which every setter reports through changes() (they are shared
|
|
7177
6377
|
// with the inline values build, so they stay as they are); this counter is
|
|
7178
6378
|
// how the two computeds hear of them. The view state below is signals and
|
|
7179
6379
|
// is read directly. The subject is this service's own and is never
|
|
7180
6380
|
// completed, so the signal listens to it exactly as long as the service
|
|
7181
6381
|
// lives.
|
|
7182
|
-
_revision = toSignal(this.
|
|
6382
|
+
_revision = toSignal(this._stateChanged$.pipe(scan((count) => count + 1, 0)), {
|
|
7183
6383
|
initialValue: 0,
|
|
7184
6384
|
manualCleanup: true,
|
|
7185
6385
|
});
|
|
@@ -7202,8 +6402,8 @@ class ReportFilterStateService {
|
|
|
7202
6402
|
// The base as apply() applied it, or null when the base is the authored
|
|
7203
6403
|
// defaults (see base).
|
|
7204
6404
|
_baseConfig = signal(null);
|
|
7205
|
-
// The temporary layer: kept apart from everything above, so snapshot()
|
|
7206
|
-
//
|
|
6405
|
+
// The temporary layer: kept apart from everything above, so snapshot()
|
|
6406
|
+
// and viewChanged never read it.
|
|
7207
6407
|
_temporary = signal(emptyTemporaryState());
|
|
7208
6408
|
// Seed groups + their defaults when a report loads. Existing session values
|
|
7209
6409
|
// for still-existing groups survive a structure re-fetch (a chat edit
|
|
@@ -7217,17 +6417,12 @@ class ReportFilterStateService {
|
|
|
7217
6417
|
// A change of the report's zone moves "today", so a value still carrying its
|
|
7218
6418
|
// group default's preset name is resolved again in the new zone. A value the
|
|
7219
6419
|
// viewer picked, or one carrying another preset name, is kept as it is.
|
|
7220
|
-
|
|
7221
|
-
// `restore` is the viewer state read from the URL. The host passes it only on
|
|
7222
|
-
// the first load of a report; a reload after a chat or dialog edit passes
|
|
7223
|
-
// null, so the in-memory state wins. Params that name a group, list or column
|
|
7224
|
-
// the report no longer has, or that do not decode, are ignored.
|
|
7225
|
-
init(report, restore = null) {
|
|
6420
|
+
init(report) {
|
|
7226
6421
|
const previousTimezone = this.timezone();
|
|
7227
6422
|
// Set first: the defaults seeded below resolve their relative presets in it.
|
|
7228
6423
|
this.timezone.set(resolveReportTimezone(report));
|
|
7229
6424
|
this.formatContext.set(resolveFormatContext(report));
|
|
7230
|
-
// Before
|
|
6425
|
+
// Before the pruning: listSearchable reads it.
|
|
7231
6426
|
this._serverSearches = report.effectiveLocale !== undefined;
|
|
7232
6427
|
const timezoneChanged = previousTimezone !== this.timezone();
|
|
7233
6428
|
const previousGroups = this._groups;
|
|
@@ -7237,13 +6432,10 @@ class ReportFilterStateService {
|
|
|
7237
6432
|
this._seed(group, previousGroups.get(group.id) ?? null, timezoneChanged);
|
|
7238
6433
|
}
|
|
7239
6434
|
this._lists = new Map(this._listComponents(report).map((component) => [component.id, component]));
|
|
7240
|
-
if (restore) {
|
|
7241
|
-
this._restore(report, restore);
|
|
7242
|
-
}
|
|
7243
6435
|
this._prune(report);
|
|
7244
6436
|
this._pruneViewState(report, previousGroups);
|
|
7245
6437
|
this._changed$.next(null);
|
|
7246
|
-
this.
|
|
6438
|
+
this._stateChanged$.next();
|
|
7247
6439
|
}
|
|
7248
6440
|
value(groupId) {
|
|
7249
6441
|
return this._values.get(groupId);
|
|
@@ -7265,11 +6457,10 @@ class ReportFilterStateService {
|
|
|
7265
6457
|
this._cleared.delete(groupId);
|
|
7266
6458
|
}
|
|
7267
6459
|
this._changed$.next(groupId);
|
|
7268
|
-
this.
|
|
6460
|
+
this._stateChanged$.next();
|
|
7269
6461
|
}
|
|
7270
6462
|
// A relative preset name as today's range in the report's zone, or null for
|
|
7271
|
-
// a name it does not know. A view's rolling range is opened through this
|
|
7272
|
-
// a link writes one as the days it gives (ReportViewService).
|
|
6463
|
+
// a name it does not know. A view's rolling range is opened through this.
|
|
7273
6464
|
resolveRelative(name) {
|
|
7274
6465
|
return this._relativeRange(name);
|
|
7275
6466
|
}
|
|
@@ -7280,7 +6471,7 @@ class ReportFilterStateService {
|
|
|
7280
6471
|
setFrequency(frequency) {
|
|
7281
6472
|
this._frequency = frequency;
|
|
7282
6473
|
this._frequencyChanged$.next();
|
|
7283
|
-
this.
|
|
6474
|
+
this._stateChanged$.next();
|
|
7284
6475
|
}
|
|
7285
6476
|
// Fires whenever the group-by grain changes — every time-series chart listens
|
|
7286
6477
|
// so it refetches and re-buckets (the override isn't scoped to any filter
|
|
@@ -7295,12 +6486,12 @@ class ReportFilterStateService {
|
|
|
7295
6486
|
}
|
|
7296
6487
|
// Records a list's sort when it is the viewer's own: a sort that is the
|
|
7297
6488
|
// list's authored first term is the list's default, not a choice, so it
|
|
7298
|
-
// clears the entry instead (the rule
|
|
6489
|
+
// clears the entry instead (the rule snapshot() applies). Otherwise a
|
|
7299
6490
|
// list's first fetch would record its seeded authored term, and a list the
|
|
7300
6491
|
// agent re-sorts later in the session would keep sending the old term
|
|
7301
6492
|
// first. Deliberately emits no change: the list sets this from the fetch
|
|
7302
6493
|
// that already ran with that sort, so a notification would only make it
|
|
7303
|
-
// refetch the same rows. Only
|
|
6494
|
+
// refetch the same rows. Only changes() hears about it.
|
|
7304
6495
|
setListSort(component, sort) {
|
|
7305
6496
|
if (sort && !sameSort(sort, listSortTerms(component.config)[0] ?? null)) {
|
|
7306
6497
|
this._listSorts.set(component.id, sort);
|
|
@@ -7308,7 +6499,7 @@ class ReportFilterStateService {
|
|
|
7308
6499
|
else {
|
|
7309
6500
|
this._listSorts.delete(component.id);
|
|
7310
6501
|
}
|
|
7311
|
-
this.
|
|
6502
|
+
this._stateChanged$.next();
|
|
7312
6503
|
}
|
|
7313
6504
|
// The text a list is searched by, or null when its search box is empty.
|
|
7314
6505
|
listSearch(componentId) {
|
|
@@ -7317,8 +6508,8 @@ class ReportFilterStateService {
|
|
|
7317
6508
|
// Whether a list shows the search box over its text columns: it does when the
|
|
7318
6509
|
// server answers a list search (see _serverSearches), unless one of its live
|
|
7319
6510
|
// filters belongs to a keyword group its header shows. A bar shows one search
|
|
7320
|
-
// box (its last keyword item), and that one is the author's. The header,
|
|
7321
|
-
//
|
|
6511
|
+
// box (its last keyword item), and that one is the author's. The header, an
|
|
6512
|
+
// opened view, the pruning, the data request state and the exports all read
|
|
7322
6513
|
// this one rule, so no search narrows a list whose screen has no box showing
|
|
7323
6514
|
// it, and no export titles a search the server did not apply.
|
|
7324
6515
|
listSearchable(component) {
|
|
@@ -7334,7 +6525,7 @@ class ReportFilterStateService {
|
|
|
7334
6525
|
}
|
|
7335
6526
|
// Records a list's search box text (listSearchText); an empty box clears
|
|
7336
6527
|
// it. The header calls this on every change of any of its items, so the
|
|
7337
|
-
// list and
|
|
6528
|
+
// list and changes() hear about it only when the stored text actually changed.
|
|
7338
6529
|
setListSearch(componentId, text) {
|
|
7339
6530
|
const search = listSearchText(text);
|
|
7340
6531
|
if (search === (this._listSearches.get(componentId) ?? '')) {
|
|
@@ -7347,7 +6538,7 @@ class ReportFilterStateService {
|
|
|
7347
6538
|
this._listSearches.delete(componentId);
|
|
7348
6539
|
}
|
|
7349
6540
|
this._listSearchChanged$.next(componentId);
|
|
7350
|
-
this.
|
|
6541
|
+
this._stateChanged$.next();
|
|
7351
6542
|
}
|
|
7352
6543
|
// The config column names a viewer hid on a list; none when it shows them all.
|
|
7353
6544
|
hiddenColumns(componentId) {
|
|
@@ -7355,7 +6546,7 @@ class ReportFilterStateService {
|
|
|
7355
6546
|
}
|
|
7356
6547
|
// Records the columns a viewer hid on a list, kept to listHiddenColumns:
|
|
7357
6548
|
// names the list cannot hide are dropped, and none (or every column) clears
|
|
7358
|
-
// the entry. Only
|
|
6549
|
+
// the entry. Only changes() hears about it, and only on a real change.
|
|
7359
6550
|
setHiddenColumns(componentId, names) {
|
|
7360
6551
|
const hidden = listHiddenColumns(this._lists.get(componentId)?.config, names);
|
|
7361
6552
|
if (sameNames(hidden, this.hiddenColumns(componentId))) {
|
|
@@ -7369,54 +6560,18 @@ class ReportFilterStateService {
|
|
|
7369
6560
|
next.delete(componentId);
|
|
7370
6561
|
}
|
|
7371
6562
|
this.listHidden.set(next);
|
|
7372
|
-
this.
|
|
6563
|
+
this._stateChanged$.next();
|
|
7373
6564
|
}
|
|
7374
6565
|
// Fires whenever one list's search text changes, so that list refetches.
|
|
7375
6566
|
listSearchChanges(componentId) {
|
|
7376
6567
|
return this._listSearchChanged$.asObservable()
|
|
7377
6568
|
.pipe(filter((id) => id === componentId), map(() => undefined));
|
|
7378
6569
|
}
|
|
7379
|
-
// Fires whenever the viewer state
|
|
7380
|
-
//
|
|
7381
|
-
// the report.
|
|
7382
|
-
|
|
7383
|
-
return this.
|
|
7384
|
-
}
|
|
7385
|
-
// The viewer state the URL should carry for `report`: only what differs from
|
|
7386
|
-
// the authored defaults, so an untouched report has a bare URL and keeps
|
|
7387
|
-
// following its defaults when they change. A group the viewer cleared is
|
|
7388
|
-
// null. A date range is compared on its days against its default resolved
|
|
7389
|
-
// today, so a value that is the default under another preset name (thisYear
|
|
7390
|
-
// against a yearToDate default) is still the default.
|
|
7391
|
-
urlSnapshot(report) {
|
|
7392
|
-
const groups = new Map();
|
|
7393
|
-
for (const group of this._groups.values()) {
|
|
7394
|
-
const value = this._values.get(group.id);
|
|
7395
|
-
if (this._cleared.has(group.id)) {
|
|
7396
|
-
groups.set(group.id, null);
|
|
7397
|
-
}
|
|
7398
|
-
else if (value && !this._isDefault(group, value)) {
|
|
7399
|
-
groups.set(group.id, value);
|
|
7400
|
-
}
|
|
7401
|
-
}
|
|
7402
|
-
const listSorts = new Map();
|
|
7403
|
-
const listSearches = new Map();
|
|
7404
|
-
const listHidden = new Map();
|
|
7405
|
-
for (const component of this._listComponents(report)) {
|
|
7406
|
-
const sort = this._listSorts.get(component.id);
|
|
7407
|
-
const search = this._listSearches.get(component.id);
|
|
7408
|
-
const hidden = this.hiddenColumns(component.id);
|
|
7409
|
-
if (sort && !sameSort(sort, listSortTerms(component.config)[0] ?? null)) {
|
|
7410
|
-
listSorts.set(component.id, sort);
|
|
7411
|
-
}
|
|
7412
|
-
if (search) {
|
|
7413
|
-
listSearches.set(component.id, search);
|
|
7414
|
-
}
|
|
7415
|
-
if (hidden.length) {
|
|
7416
|
-
listHidden.set(component.id, [...hidden]);
|
|
7417
|
-
}
|
|
7418
|
-
}
|
|
7419
|
-
return { groups, listSorts, listSearches, listHidden, frequency: this._frequency };
|
|
6570
|
+
// Fires whenever the viewer state may have changed: a group value, the
|
|
6571
|
+
// grain, the match, the page, a list sort, search or hidden columns, a
|
|
6572
|
+
// drill path, an applied view or a (re)load of the report.
|
|
6573
|
+
changes() {
|
|
6574
|
+
return this._stateChanged$.asObservable();
|
|
7420
6575
|
}
|
|
7421
6576
|
isFilterDisabled(filterId) {
|
|
7422
6577
|
return this._sessionDisabled.has(filterId);
|
|
@@ -7432,7 +6587,7 @@ class ReportFilterStateService {
|
|
|
7432
6587
|
// the search with it. The list refetches on the group change below.
|
|
7433
6588
|
this._dropUnsearchable();
|
|
7434
6589
|
this._changed$.next(groupId);
|
|
7435
|
-
this.
|
|
6590
|
+
this._stateChanged$.next();
|
|
7436
6591
|
}
|
|
7437
6592
|
// Notifications relevant to ONE component: any change to a group one of its
|
|
7438
6593
|
// filters belongs to (or a full re-init).
|
|
@@ -7504,7 +6659,7 @@ class ReportFilterStateService {
|
|
|
7504
6659
|
return;
|
|
7505
6660
|
}
|
|
7506
6661
|
this._pageId.set(pageId);
|
|
7507
|
-
this.
|
|
6662
|
+
this._stateChanged$.next();
|
|
7508
6663
|
}
|
|
7509
6664
|
// A change of match can change every tile a group with a value narrows, so
|
|
7510
6665
|
// every tile hears about it.
|
|
@@ -7515,7 +6670,7 @@ class ReportFilterStateService {
|
|
|
7515
6670
|
}
|
|
7516
6671
|
this._match.set(next);
|
|
7517
6672
|
this._changed$.next(null);
|
|
7518
|
-
this.
|
|
6673
|
+
this._stateChanged$.next();
|
|
7519
6674
|
}
|
|
7520
6675
|
// A chart's drill path, top first; empty while the chart is at its top.
|
|
7521
6676
|
// It reads a signal, so a template that shows it repaints when it moves.
|
|
@@ -7524,7 +6679,7 @@ class ReportFilterStateService {
|
|
|
7524
6679
|
}
|
|
7525
6680
|
// Records a chart's drill path, kept to cleanDrillPath; an empty path is
|
|
7526
6681
|
// the chart's top. Only a chart of the open report drills: a path for any
|
|
7527
|
-
// other component is ignored. Only that chart and
|
|
6682
|
+
// other component is ignored. Only that chart and changes() hear about it,
|
|
7528
6683
|
// and only on a real change.
|
|
7529
6684
|
setDrill(componentId, path) {
|
|
7530
6685
|
const kept = cleanDrillPath(path);
|
|
@@ -7533,7 +6688,7 @@ class ReportFilterStateService {
|
|
|
7533
6688
|
}
|
|
7534
6689
|
this._setDrillPath(componentId, kept);
|
|
7535
6690
|
this._drillChanged$.next(componentId);
|
|
7536
|
-
this.
|
|
6691
|
+
this._stateChanged$.next();
|
|
7537
6692
|
}
|
|
7538
6693
|
// Fires whenever one chart's drill path changes, so that chart refetches.
|
|
7539
6694
|
drillChanges(componentId) {
|
|
@@ -7541,16 +6696,16 @@ class ReportFilterStateService {
|
|
|
7541
6696
|
.pipe(filter((id) => id === componentId), map(() => undefined));
|
|
7542
6697
|
}
|
|
7543
6698
|
// Takes the drill path a chart's answer applied as the chart's own: the
|
|
7544
|
-
// server keeps the longest part of a sent path it can apply, so
|
|
7545
|
-
//
|
|
7546
|
-
//
|
|
6699
|
+
// server keeps the longest part of a sent path it can apply, so a later
|
|
6700
|
+
// save holds that part only. The chart has just read that path, so only
|
|
6701
|
+
// changes() hears about it. True when the path changed.
|
|
7547
6702
|
adoptDrill(componentId, path) {
|
|
7548
6703
|
const kept = cleanDrillPath(path);
|
|
7549
6704
|
if (!this._isChart(componentId) || sameDrillPath(kept, this.drill(componentId))) {
|
|
7550
6705
|
return false;
|
|
7551
6706
|
}
|
|
7552
6707
|
this._setDrillPath(componentId, kept);
|
|
7553
|
-
this.
|
|
6708
|
+
this._stateChanged$.next();
|
|
7554
6709
|
return true;
|
|
7555
6710
|
}
|
|
7556
6711
|
// The durable state on screen as one view document: the page, every group
|
|
@@ -7587,32 +6742,18 @@ class ReportFilterStateService {
|
|
|
7587
6742
|
return { v: REPORT_VIEW_CONFIG_VERSION, groups };
|
|
7588
6743
|
}
|
|
7589
6744
|
// Replaces the durable state with a view's (`config`; null = the report's
|
|
7590
|
-
// authored defaults),
|
|
7591
|
-
// (
|
|
7592
|
-
//
|
|
7593
|
-
// group's type, is skipped.
|
|
7594
|
-
//
|
|
7595
|
-
//
|
|
7596
|
-
//
|
|
7597
|
-
|
|
7598
|
-
// another (null = the authored defaults). So:
|
|
7599
|
-
// - a shared view's link (rv): apply(sharedView, diff);
|
|
7600
|
-
// - a personal view's link (rpv): apply(null, fullDiff, personalView). The
|
|
7601
|
-
// screen is the authored defaults plus the full diff, so a key the diff
|
|
7602
|
-
// leaves out (the match, the grain, a tile's sort) is the authored one,
|
|
7603
|
-
// and the personal view is still the base.
|
|
7604
|
-
// Every change is announced once, at the end.
|
|
7605
|
-
apply(config, urlDiff = null, base = config) {
|
|
6745
|
+
// authored defaults), which becomes the base: what the screen is compared
|
|
6746
|
+
// with for '(edited)' and what resetToBase() returns to. A group the view
|
|
6747
|
+
// does not hold opens at its authored default; one the report does not
|
|
6748
|
+
// have, or whose value does not fit the group's type, is skipped. The
|
|
6749
|
+
// view's page opens, unless `pageId` names a page of the report (a ?page=
|
|
6750
|
+
// the report opened with beats its default view's page). The temporary
|
|
6751
|
+
// layer is kept. Every change is announced once, at the end.
|
|
6752
|
+
apply(config, pageId = null) {
|
|
7606
6753
|
const frequency = this._frequency;
|
|
7607
|
-
this._setBase(
|
|
7608
|
-
if (config !== base) {
|
|
7609
|
-
this._applyLayer(config, true);
|
|
7610
|
-
}
|
|
6754
|
+
this._setBase(config);
|
|
7611
6755
|
this._applyPage(config?.pageId);
|
|
7612
|
-
|
|
7613
|
-
this._applyLayer(urlDiff, false);
|
|
7614
|
-
this._applyPage(urlDiff.pageId);
|
|
7615
|
-
}
|
|
6756
|
+
this._applyPage(pageId);
|
|
7616
6757
|
this._appliedVersion.update((version) => version + 1);
|
|
7617
6758
|
this._notifyApplied(frequency);
|
|
7618
6759
|
}
|
|
@@ -7624,7 +6765,7 @@ class ReportFilterStateService {
|
|
|
7624
6765
|
const frequency = this._frequency;
|
|
7625
6766
|
const sorts = new Map(this._listSorts);
|
|
7626
6767
|
this._temporary.set(emptyTemporaryState());
|
|
7627
|
-
this._applyLayer(this._baseConfig()
|
|
6768
|
+
this._applyLayer(this._baseConfig());
|
|
7628
6769
|
this._countSortResets(sorts);
|
|
7629
6770
|
this._notifyApplied(frequency);
|
|
7630
6771
|
}
|
|
@@ -7632,13 +6773,13 @@ class ReportFilterStateService {
|
|
|
7632
6773
|
// just saved from the screen holds. The page on screen is the base's.
|
|
7633
6774
|
setBaseToScreen() {
|
|
7634
6775
|
this._baseConfig.set(this.snapshot());
|
|
7635
|
-
this.
|
|
6776
|
+
this._stateChanged$.next();
|
|
7636
6777
|
}
|
|
7637
6778
|
// The report's authored defaults become the base again (the open view was
|
|
7638
6779
|
// deleted); the screen stays as it is.
|
|
7639
6780
|
clearBase() {
|
|
7640
6781
|
this._baseConfig.set(null);
|
|
7641
|
-
this.
|
|
6782
|
+
this._stateChanged$.next();
|
|
7642
6783
|
}
|
|
7643
6784
|
// Puts only `groupIds` back on the base: the open view's value for a group
|
|
7644
6785
|
// it holds, else the group's authored default. A tile header's 'Clear
|
|
@@ -7657,7 +6798,7 @@ class ReportFilterStateService {
|
|
|
7657
6798
|
}
|
|
7658
6799
|
this._changed$.next(groupId);
|
|
7659
6800
|
});
|
|
7660
|
-
this.
|
|
6801
|
+
this._stateChanged$.next();
|
|
7661
6802
|
}
|
|
7662
6803
|
// Whether one of `groupIds` shows a value other than its base: the open
|
|
7663
6804
|
// view's value for a group it holds, else the group's authored default,
|
|
@@ -7680,12 +6821,6 @@ class ReportFilterStateService {
|
|
|
7680
6821
|
return !sameStoredGroupValue(base, screen);
|
|
7681
6822
|
});
|
|
7682
6823
|
}
|
|
7683
|
-
// Asks the host's URL writer to write again, with nothing on screen
|
|
7684
|
-
// changed: sharing the open view, or no longer sharing it, moves the key
|
|
7685
|
-
// that names it in a link (rv or rpv).
|
|
7686
|
-
rewriteUrl() {
|
|
7687
|
-
this._urlChanged$.next();
|
|
7688
|
-
}
|
|
7689
6824
|
// The interaction state a tile's data request carries now: tileDataState
|
|
7690
6825
|
// over the open report and the screen's snapshot().
|
|
7691
6826
|
tileState(component) {
|
|
@@ -7767,65 +6902,6 @@ class ReportFilterStateService {
|
|
|
7767
6902
|
this._values.delete(group.id);
|
|
7768
6903
|
}
|
|
7769
6904
|
}
|
|
7770
|
-
// Applies the viewer state read from the URL on top of the seeded defaults.
|
|
7771
|
-
_restore(report, restore) {
|
|
7772
|
-
this._restoreGroups(restore);
|
|
7773
|
-
this._restoreListSorts(report, restore);
|
|
7774
|
-
this._restoreListSearches(report, restore);
|
|
7775
|
-
this._restoreListHidden(restore);
|
|
7776
|
-
// The URL is the whole truth on a first load: no grain in it means the
|
|
7777
|
-
// authored ones. The host still drops a grain the report does not offer.
|
|
7778
|
-
this._frequency = GRANULARITY_OPTIONS
|
|
7779
|
-
.find((option) => option.value === restore.groupBy)?.value ?? null;
|
|
7780
|
-
}
|
|
7781
|
-
// The URL's group values, applied as a view's are. An empty one is an
|
|
7782
|
-
// explicit clear of the group.
|
|
7783
|
-
_restoreGroups(restore) {
|
|
7784
|
-
restore.groups.forEach((stored, groupId) => {
|
|
7785
|
-
const group = this._groups.get(groupId);
|
|
7786
|
-
if (group) {
|
|
7787
|
-
this._applyGroup(group, stored);
|
|
7788
|
-
}
|
|
7789
|
-
});
|
|
7790
|
-
}
|
|
7791
|
-
// The URL's list sorts, each kept only for a list the report has and a
|
|
7792
|
-
// column that list can be sorted by.
|
|
7793
|
-
_restoreListSorts(report, restore) {
|
|
7794
|
-
const lists = new Map(this._listComponents(report).map((component) => [component.id, component]));
|
|
7795
|
-
restore.listSorts.forEach((raw, componentId) => {
|
|
7796
|
-
const component = lists.get(componentId);
|
|
7797
|
-
const sort = decodeSort(raw);
|
|
7798
|
-
if (component && sort && this._sortableColumns(component).includes(sort.column)) {
|
|
7799
|
-
this._listSorts.set(componentId, sort);
|
|
7800
|
-
}
|
|
7801
|
-
});
|
|
7802
|
-
}
|
|
7803
|
-
// The URL's list searches, each kept only for a list the report has that
|
|
7804
|
-
// shows a search box (listSearchable). The text is read as listSearchText,
|
|
7805
|
-
// and an empty one is no search.
|
|
7806
|
-
_restoreListSearches(report, restore) {
|
|
7807
|
-
const lists = new Map(this._listComponents(report).map((component) => [component.id, component]));
|
|
7808
|
-
restore.listSearches.forEach((raw, componentId) => {
|
|
7809
|
-
const component = lists.get(componentId);
|
|
7810
|
-
const search = listSearchText(raw);
|
|
7811
|
-
if (component && this.listSearchable(component) && search) {
|
|
7812
|
-
this._listSearches.set(componentId, search);
|
|
7813
|
-
}
|
|
7814
|
-
});
|
|
7815
|
-
}
|
|
7816
|
-
// The URL's hidden columns, each list's kept to listHiddenColumns: only the
|
|
7817
|
-
// configured columns of a plain list the report has, and none when they
|
|
7818
|
-
// would hide every column.
|
|
7819
|
-
_restoreListHidden(restore) {
|
|
7820
|
-
const next = new Map(this.listHidden());
|
|
7821
|
-
restore.listHidden.forEach((raw, componentId) => {
|
|
7822
|
-
const hidden = listHiddenColumns(this._lists.get(componentId)?.config, decodeColumnList(raw));
|
|
7823
|
-
if (hidden.length) {
|
|
7824
|
-
next.set(componentId, hidden);
|
|
7825
|
-
}
|
|
7826
|
-
});
|
|
7827
|
-
this.listHidden.set(next);
|
|
7828
|
-
}
|
|
7829
6905
|
// Drops the search of every list that no longer shows a search box: one
|
|
7830
6906
|
// the report no longer has, or one whose header now has a keyword group.
|
|
7831
6907
|
_dropUnsearchable() {
|
|
@@ -7885,33 +6961,6 @@ class ReportFilterStateService {
|
|
|
7885
6961
|
? { ...fixed, preset: stored.preset }
|
|
7886
6962
|
: fixed;
|
|
7887
6963
|
}
|
|
7888
|
-
// Whether a value is the group's authored default, by type: a date range on
|
|
7889
|
-
// its days (the default resolved today), a select on its values in any
|
|
7890
|
-
// order, a keyword on its text.
|
|
7891
|
-
_isDefault(group, value) {
|
|
7892
|
-
const defaultValue = this._defaultValue(group);
|
|
7893
|
-
if (!defaultValue) {
|
|
7894
|
-
return false;
|
|
7895
|
-
}
|
|
7896
|
-
switch (group.type) {
|
|
7897
|
-
case 'dateRange':
|
|
7898
|
-
// An untouched rolling default still carries its preset name. Its days
|
|
7899
|
-
// were resolved when it was seeded, and after midnight they no longer
|
|
7900
|
-
// match today's, but it is still the default and stays out of the URL.
|
|
7901
|
-
if (value.preset && value.preset === group.config?.default?.relative) {
|
|
7902
|
-
return true;
|
|
7903
|
-
}
|
|
7904
|
-
return sameDays(value, defaultValue);
|
|
7905
|
-
case 'select':
|
|
7906
|
-
return samePicks(value.values, defaultValue.values)
|
|
7907
|
-
&& samePicks(value.excludeValues, defaultValue.excludeValues);
|
|
7908
|
-
case 'numberRange':
|
|
7909
|
-
return finiteNumber(value.min) === finiteNumber(defaultValue.min)
|
|
7910
|
-
&& finiteNumber(value.max) === finiteNumber(defaultValue.max);
|
|
7911
|
-
default:
|
|
7912
|
-
return (value.value ?? '').trim() === (defaultValue.value ?? '').trim();
|
|
7913
|
-
}
|
|
7914
|
-
}
|
|
7915
6964
|
_listComponents(report) {
|
|
7916
6965
|
return reportComponents(report)
|
|
7917
6966
|
.filter((component) => component.type === 'list');
|
|
@@ -8036,13 +7085,13 @@ class ReportFilterStateService {
|
|
|
8036
7085
|
}
|
|
8037
7086
|
return tiles;
|
|
8038
7087
|
}
|
|
8039
|
-
// The
|
|
8040
|
-
//
|
|
8041
|
-
//
|
|
8042
|
-
//
|
|
8043
|
-
//
|
|
7088
|
+
// The view apply() was given, applied as the screen's state and kept as
|
|
7089
|
+
// the base. What is kept is the snapshot of it, with only the groups it
|
|
7090
|
+
// held: a group it did not hold, or held in a shape that does not fit,
|
|
7091
|
+
// follows its authored default (viewDiffers compares it with
|
|
7092
|
+
// authoredConfig()).
|
|
8044
7093
|
_setBase(base) {
|
|
8045
|
-
const held = this._applyLayer(base
|
|
7094
|
+
const held = this._applyLayer(base);
|
|
8046
7095
|
if (!base) {
|
|
8047
7096
|
this._baseConfig.set(null);
|
|
8048
7097
|
return;
|
|
@@ -8057,56 +7106,45 @@ class ReportFilterStateService {
|
|
|
8057
7106
|
}
|
|
8058
7107
|
this._baseConfig.set(applied);
|
|
8059
7108
|
}
|
|
8060
|
-
// Applies
|
|
8061
|
-
//
|
|
8062
|
-
//
|
|
8063
|
-
//
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
const match = config?.match;
|
|
8068
|
-
if (reset || match === 'all' || match === 'any') {
|
|
8069
|
-
this._match.set(match === 'any' ? 'any' : 'all');
|
|
8070
|
-
}
|
|
7109
|
+
// Applies a view's state in place of the whole durable state, and gives the
|
|
7110
|
+
// ids of the groups it set: what `config` does not hold goes back to the
|
|
7111
|
+
// authored report. Nothing is announced here: the caller announces once
|
|
7112
|
+
// (_notifyApplied).
|
|
7113
|
+
_applyLayer(config) {
|
|
7114
|
+
const held = this._applyGroups(config?.groups ?? {});
|
|
7115
|
+
this._match.set(config?.match === 'any' ? 'any' : 'all');
|
|
8071
7116
|
const groupBy = config?.groupBy;
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
}
|
|
8075
|
-
if (reset) {
|
|
8076
|
-
this._clearTiles();
|
|
8077
|
-
}
|
|
7117
|
+
this._frequency = isChartGranularity(groupBy) ? groupBy : null;
|
|
7118
|
+
this._clearTiles();
|
|
8078
7119
|
Object.entries(config?.tiles ?? {})
|
|
8079
7120
|
.forEach(([componentId, tile]) => this._applyTile(Number(componentId), tile ?? {}));
|
|
8080
7121
|
return held;
|
|
8081
7122
|
}
|
|
8082
|
-
// Each group's value from a
|
|
8083
|
-
//
|
|
8084
|
-
|
|
8085
|
-
// keep it rolling (_rollingValue), as an untouched range does in the bar.
|
|
8086
|
-
_applyGroups(groups, reset) {
|
|
7123
|
+
// Each group's value from a view; a group it does not set goes back to its
|
|
7124
|
+
// authored default.
|
|
7125
|
+
_applyGroups(groups) {
|
|
8087
7126
|
const held = new Set();
|
|
8088
7127
|
this._groups.forEach((group) => {
|
|
8089
|
-
if (Object.prototype.hasOwnProperty.call(groups, group.id) && this._applyGroup(group, groups[group.id]
|
|
7128
|
+
if (Object.prototype.hasOwnProperty.call(groups, group.id) && this._applyGroup(group, groups[group.id])) {
|
|
8090
7129
|
held.add(group.id);
|
|
8091
7130
|
}
|
|
8092
|
-
else
|
|
7131
|
+
else {
|
|
8093
7132
|
this._seedAuthored(group);
|
|
8094
7133
|
}
|
|
8095
7134
|
});
|
|
8096
7135
|
return held;
|
|
8097
7136
|
}
|
|
8098
7137
|
// One group's stored value, or false when the group's type cannot hold it
|
|
8099
|
-
// (it is then dropped whole, and the
|
|
8100
|
-
//
|
|
8101
|
-
|
|
8102
|
-
_applyGroup(group, stored, keepRolling = false) {
|
|
7138
|
+
// (it is then dropped whole, and the caller seeds the group's authored
|
|
7139
|
+
// default). null, or a value that holds nothing, clears the group.
|
|
7140
|
+
_applyGroup(group, stored) {
|
|
8103
7141
|
const kept = stored === null ? {} : groupValueFromStored(stored, group.type);
|
|
8104
7142
|
if (kept === null) {
|
|
8105
7143
|
return false;
|
|
8106
7144
|
}
|
|
8107
7145
|
const value = toLiveGroupValue(group.type, kept, (name) => this.resolveRelative(name));
|
|
8108
7146
|
if (value) {
|
|
8109
|
-
this._values.set(group.id,
|
|
7147
|
+
this._values.set(group.id, value);
|
|
8110
7148
|
this._cleared.delete(group.id);
|
|
8111
7149
|
}
|
|
8112
7150
|
else {
|
|
@@ -8118,15 +7156,6 @@ class ReportFilterStateService {
|
|
|
8118
7156
|
}
|
|
8119
7157
|
return true;
|
|
8120
7158
|
}
|
|
8121
|
-
// The rolling range a link's days are compared with: the one the base
|
|
8122
|
-
// holds for the group (a view's, resolved today), else the value on screen
|
|
8123
|
-
// (the authored default the base layer seeded).
|
|
8124
|
-
_rollingValue(groupId) {
|
|
8125
|
-
const held = this._baseConfig()?.groups?.[groupId];
|
|
8126
|
-
return held?.relative
|
|
8127
|
-
? toLiveGroupValue('dateRange', held, (name) => this.resolveRelative(name)) ?? undefined
|
|
8128
|
-
: this._values.get(groupId);
|
|
8129
|
-
}
|
|
8130
7159
|
// A group back at its authored default.
|
|
8131
7160
|
_seedAuthored(group) {
|
|
8132
7161
|
const value = this._defaultValue(group);
|
|
@@ -8145,9 +7174,9 @@ class ReportFilterStateService {
|
|
|
8145
7174
|
this.listHidden.set(new Map());
|
|
8146
7175
|
this._drills.set(new Map());
|
|
8147
7176
|
}
|
|
8148
|
-
// One tile's stored state, each key checked
|
|
8149
|
-
//
|
|
8150
|
-
//
|
|
7177
|
+
// One tile's stored state, each key checked: a sort on a column the list
|
|
7178
|
+
// can sort by, a search on a list that shows the box, hidden columns the
|
|
7179
|
+
// list can hide, a drill path on a chart.
|
|
8151
7180
|
_applyTile(componentId, tile) {
|
|
8152
7181
|
const list = this._lists.get(componentId);
|
|
8153
7182
|
if (list) {
|
|
@@ -8218,7 +7247,7 @@ class ReportFilterStateService {
|
|
|
8218
7247
|
if (frequency !== this._frequency) {
|
|
8219
7248
|
this._frequencyChanged$.next();
|
|
8220
7249
|
}
|
|
8221
|
-
this.
|
|
7250
|
+
this._stateChanged$.next();
|
|
8222
7251
|
}
|
|
8223
7252
|
// The tiles of the report init() last read.
|
|
8224
7253
|
_tiles() {
|
|
@@ -9671,8 +8700,8 @@ class ComponentListComponent {
|
|
|
9671
8700
|
// How many leading columns stay in view while the list scrolls sideways.
|
|
9672
8701
|
pinned = computed(() => listPinnedColumns(this.component().config));
|
|
9673
8702
|
columnClasses = columnClasses;
|
|
9674
|
-
// The config columns the viewer hid on this list (the Columns dialog, or
|
|
9675
|
-
//
|
|
8703
|
+
// The config columns the viewer hid on this list (the Columns dialog, or a
|
|
8704
|
+
// saved view). fs-list hides and shows them in place through [show], so the rows
|
|
9676
8705
|
// already fetched stand.
|
|
9677
8706
|
hidden = computed(() => new Set(this._filterState.hiddenColumns(this.component().id)));
|
|
9678
8707
|
totalLabel = LIST_TOTAL_LABEL;
|
|
@@ -9747,8 +8776,8 @@ class ComponentListComponent {
|
|
|
9747
8776
|
status: false,
|
|
9748
8777
|
reload: false,
|
|
9749
8778
|
// FsList never touches the URL or persists sort/paging state. The sort
|
|
9750
|
-
//
|
|
9751
|
-
//
|
|
8779
|
+
// is kept in ReportFilterStateService (setListSort), and paging stays
|
|
8780
|
+
// out of it.
|
|
9752
8781
|
queryParam: false,
|
|
9753
8782
|
persist: false,
|
|
9754
8783
|
sort: sort
|
|
@@ -10887,7 +9916,7 @@ class ReportComponentComponent {
|
|
|
10887
9916
|
// The breadcrumb after the title while the chart is drilled ('2026', 'Q3'),
|
|
10888
9917
|
// one label per step of its path, in the report's locale. Empty at the top
|
|
10889
9918
|
// and while the viewer is not offered drill: a path an older server never
|
|
10890
|
-
// applies is kept for
|
|
9919
|
+
// applies is kept for a saved view, but not shown.
|
|
10891
9920
|
drillCrumbs = computed(() => (this._offeredFeatures().includes('drill')
|
|
10892
9921
|
? drillCrumbLabels(this._filterState.drill(this.component?.id), this._filterState.formatContext())
|
|
10893
9922
|
: []));
|
|
@@ -11083,8 +10112,8 @@ class ReportComponentComponent {
|
|
|
11083
10112
|
return this.component.type === 'list' && listHideableColumns(this.component.config).length > 1;
|
|
11084
10113
|
}
|
|
11085
10114
|
// Which of a plain list's columns show. The choice is the viewer's (kept in
|
|
11086
|
-
// the
|
|
11087
|
-
// place, with no refetch.
|
|
10115
|
+
// the report's state and a saved view, never saved to the report), and the
|
|
10116
|
+
// list hides the columns in place, with no refetch.
|
|
11088
10117
|
chooseColumns() {
|
|
11089
10118
|
const columns = (this.component.config?.columns ?? [])
|
|
11090
10119
|
.map((column) => ({ column: column.column, label: column.label || column.column }));
|
|
@@ -11283,9 +10312,9 @@ class ReportComponentComponent {
|
|
|
11283
10312
|
this._headerValues.set([...reported.values()].map(groupValueSignature).join('|'));
|
|
11284
10313
|
this._headerVersion.set(this._filterState.stateVersion());
|
|
11285
10314
|
this.filterConfig.set({
|
|
11286
|
-
// fs-filter never touches the URL or persists filter state
|
|
11287
|
-
//
|
|
11288
|
-
//
|
|
10315
|
+
// fs-filter never touches the URL or persists filter state: a report
|
|
10316
|
+
// keeps no state in the URL. The values are owned by
|
|
10317
|
+
// ReportFilterStateService.
|
|
11289
10318
|
queryParam: false,
|
|
11290
10319
|
persist: false,
|
|
11291
10320
|
items: [
|
|
@@ -11296,7 +10325,7 @@ class ReportComponentComponent {
|
|
|
11296
10325
|
// a stale group value back, and a group change must not refetch through
|
|
11297
10326
|
// the search.
|
|
11298
10327
|
change: (query) => {
|
|
11299
|
-
const values = groupValuesFromQuery(query ?? {}, this.componentFilterGroups
|
|
10328
|
+
const values = groupValuesFromQuery(query ?? {}, this.componentFilterGroups);
|
|
11300
10329
|
for (const { groupId, value } of movedGroupValues(reported, values)) {
|
|
11301
10330
|
this._filterState.setValue(groupId, value);
|
|
11302
10331
|
}
|
|
@@ -11460,7 +10489,7 @@ class ReportComponentComponent {
|
|
|
11460
10489
|
}
|
|
11461
10490
|
// A chart takes the drill path its answer applied (adoptedDrillPath) while
|
|
11462
10491
|
// the viewer is offered drill; an older server applies none, and the path
|
|
11463
|
-
// stays for
|
|
10492
|
+
// stays for a saved view. When the path it sent was cut, the viewer is told
|
|
11464
10493
|
// once (drillResetNoted): the shorter path is what the chart reads from
|
|
11465
10494
|
// now on, so the server cuts nothing again.
|
|
11466
10495
|
_adoptDrill(data) {
|
|
@@ -11588,9 +10617,10 @@ function sameTokens(a, b) {
|
|
|
11588
10617
|
class ReportCanvasComponent {
|
|
11589
10618
|
report;
|
|
11590
10619
|
editMode = false;
|
|
11591
|
-
// The page the host wants on screen, by page id (a
|
|
11592
|
-
//
|
|
11593
|
-
//
|
|
10620
|
+
// The page the host wants on screen, by page id (a ?page= the report opened
|
|
10621
|
+
// with, or an opened view's page). Keyed on id rather than index so it
|
|
10622
|
+
// survives pages being reordered or deleted; an id that no longer resolves
|
|
10623
|
+
// falls back to the first page.
|
|
11594
10624
|
pageId = null;
|
|
11595
10625
|
// A component asked for its settings dialog — the page-level host opens it
|
|
11596
10626
|
// (it owns MatDialog + the post-save reload).
|
|
@@ -11598,8 +10628,8 @@ class ReportCanvasComponent {
|
|
|
11598
10628
|
// The report's structure changed here (a page was added) — the host re-fetches
|
|
11599
10629
|
// it (it owns the assembled report + filter state).
|
|
11600
10630
|
reportChanged = new EventEmitter();
|
|
11601
|
-
// The page on screen changed — the host
|
|
11602
|
-
//
|
|
10631
|
+
// The page on screen changed — the host keeps it in the report's state (a
|
|
10632
|
+
// saved view stores it). Carries the page id, never the index.
|
|
11603
10633
|
pageChanged = new EventEmitter();
|
|
11604
10634
|
zoomPan;
|
|
11605
10635
|
// The rendered components of the active page — only the canvas can reach the
|
|
@@ -11610,7 +10640,7 @@ class ReportCanvasComponent {
|
|
|
11610
10640
|
PX = PX_PER_INCH;
|
|
11611
10641
|
// The report's pages as the canvas draws them. This is the host's own
|
|
11612
10642
|
// `report.pages` array, patched in place (a rename, a delete, a reorder) so
|
|
11613
|
-
// the host's exports
|
|
10643
|
+
// the host's exports see the change too. Because the array
|
|
11614
10644
|
// and its pages keep their identity across a patch, the signal notifies on
|
|
11615
10645
|
// every set rather than only when the reference changes.
|
|
11616
10646
|
pages = signal([], { equal: () => false });
|
|
@@ -11697,7 +10727,7 @@ class ReportCanvasComponent {
|
|
|
11697
10727
|
}
|
|
11698
10728
|
this.groups = new Map((this.report?.filterGroups ?? []).map((group) => [group.id, group]));
|
|
11699
10729
|
this.pages.set(this.report?.pages ?? []);
|
|
11700
|
-
// A page just added in this tab, else the
|
|
10730
|
+
// A page just added in this tab, else the asked-for page, else the page
|
|
11701
10731
|
// shown (the first page on another report). See canvasLandingPage.
|
|
11702
10732
|
const reportId = this.report?.id ?? null;
|
|
11703
10733
|
this.activePageIndex.set(canvasLandingPage({
|
|
@@ -11906,8 +10936,8 @@ class ReportCanvasComponent {
|
|
|
11906
10936
|
this.activePageIndex.set(Math.min(index, pages.length - 1));
|
|
11907
10937
|
this._previousPageCount = pages.length;
|
|
11908
10938
|
this.selectedComponentId.set(null);
|
|
11909
|
-
// The
|
|
11910
|
-
//
|
|
10939
|
+
// The page on screen just stopped existing — report the neighbour we
|
|
10940
|
+
// landed on, so the host's state names a page the report has.
|
|
11911
10941
|
this.pageChanged.emit(this.activePage?.id ?? null);
|
|
11912
10942
|
this._message.success('Deleted page');
|
|
11913
10943
|
});
|
|
@@ -12014,12 +11044,12 @@ class ReportCanvasComponent {
|
|
|
12014
11044
|
}
|
|
12015
11045
|
// Triggers landing together (a reload that also changes the page) make one
|
|
12016
11046
|
// request, and a newer request drops the one still in flight. Any other
|
|
12017
|
-
// change of the screen's state (
|
|
11047
|
+
// change of the screen's state (changes) asks again only when it moved
|
|
12018
11048
|
// the request: a view applied or the bar reset (stateVersion moves on
|
|
12019
11049
|
// each), the match changed, or, while the groups match 'any', a group the
|
|
12020
11050
|
// values have no member of took or lost a value.
|
|
12021
11051
|
_watchValues(layer, onScreen$) {
|
|
12022
|
-
merge(layer.reload$, onScreen$, this._filterState.changesForAny(() => layer.source()?.values ?? []).pipe(debounceTime(300)), this._filterState.
|
|
11052
|
+
merge(layer.reload$, onScreen$, this._filterState.changesForAny(() => layer.source()?.values ?? []).pipe(debounceTime(300)), this._filterState.changes().pipe(debounceTime(300), filter$1(() => this._requestMoved(layer))))
|
|
12023
11053
|
.pipe(debounceTime(0), map$1(() => this._layerRequest(layer.source())), tap$1((request) => layer.pending.set(request)), switchMap$1((request) => this._loadValues(request)), tap$1((loaded) => {
|
|
12024
11054
|
layer.results.set(loaded);
|
|
12025
11055
|
layer.pending.set(null);
|
|
@@ -12331,6 +11361,13 @@ const COMPONENT_TOGGLE_TOOLTIP = 'Show on this component';
|
|
|
12331
11361
|
const FIXED_DATES_PERIOD = 'fixed';
|
|
12332
11362
|
// How many decimals a Default minimum or Default maximum takes.
|
|
12333
11363
|
const DEFAULT_BOUND_SCALE = 6;
|
|
11364
|
+
// A filter's Name and Label, checked as the server checks them, so a bad one
|
|
11365
|
+
// is caught before Save. The name is an identifier: a letter, then letters
|
|
11366
|
+
// and digits. That it is unique in the report is the server's to check.
|
|
11367
|
+
const FILTER_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
11368
|
+
const FILTER_NAME_PATTERN_MESSAGE = 'A name starts with a letter and holds only letters and digits.';
|
|
11369
|
+
const FILTER_NAME_MAX_LENGTH = 50;
|
|
11370
|
+
const FILTER_LABEL_MAX_LENGTH = 200;
|
|
12334
11371
|
// Every Report bar choice, in the order the select lists them.
|
|
12335
11372
|
const BAR_PLACEMENT_OPTIONS = [
|
|
12336
11373
|
{ value: 'off', name: 'Off' },
|
|
@@ -13235,7 +12272,11 @@ function currencyLabel(code) {
|
|
|
13235
12272
|
// default month + organization scopes the landing page from the first load.
|
|
13236
12273
|
// The rules of the rows live in filter-settings-rules.ts.
|
|
13237
12274
|
class ReportSettingsComponent {
|
|
13238
|
-
|
|
12275
|
+
// A signal, so a refused Label or Name can bring the Filters tab forward.
|
|
12276
|
+
selectedTab = signal('settings');
|
|
12277
|
+
// True once any save request has been stored, even when the save as a
|
|
12278
|
+
// whole then failed: the page reloads after a dismissed dialog too.
|
|
12279
|
+
wrote = signal(false);
|
|
13239
12280
|
name = '';
|
|
13240
12281
|
// The page in inches. No initializer: every report carries a size, and
|
|
13241
12282
|
// ngOnInit seeds both before the tab renders, so a placeholder here could only
|
|
@@ -13317,6 +12358,12 @@ class ReportSettingsComponent {
|
|
|
13317
12358
|
filterRows = signal([]);
|
|
13318
12359
|
fixedDatesPeriod = FIXED_DATES_PERIOD;
|
|
13319
12360
|
boundScale = DEFAULT_BOUND_SCALE;
|
|
12361
|
+
namePattern = FILTER_NAME_PATTERN;
|
|
12362
|
+
namePatternMessage = FILTER_NAME_PATTERN_MESSAGE;
|
|
12363
|
+
nameMaxLength = FILTER_NAME_MAX_LENGTH;
|
|
12364
|
+
labelMaxLength = FILTER_LABEL_MAX_LENGTH;
|
|
12365
|
+
labelInputId = labelInputId;
|
|
12366
|
+
nameInputId = nameInputId;
|
|
13320
12367
|
// The Default minimum and Default maximum checks, through fsForm. Each field
|
|
13321
12368
|
// gets the other end as its validateData, so typing in one checks the pair
|
|
13322
12369
|
// again; boundsError words the refusal as the server does.
|
|
@@ -13326,12 +12373,27 @@ class ReportSettingsComponent {
|
|
|
13326
12373
|
maxBoundValidator = (control, min) => {
|
|
13327
12374
|
this._throwBoundsError(min, control.value);
|
|
13328
12375
|
};
|
|
12376
|
+
// The server's refusal of a Label or Name, through fsForm. The shape and
|
|
12377
|
+
// length are checked on the field (fsFormPattern, fsFormMaxLength), but only
|
|
12378
|
+
// the server knows the report's other names, so the field shows the
|
|
12379
|
+
// server's own words while it still holds the refused value. A new refusal
|
|
12380
|
+
// arrives as the field's validateData, which checks the field again.
|
|
12381
|
+
refusalValidator = (control, refusal) => {
|
|
12382
|
+
if (refusal && String(control.value ?? '').trim() === refusal.value) {
|
|
12383
|
+
throw new Error(refusal.message);
|
|
12384
|
+
}
|
|
12385
|
+
};
|
|
13329
12386
|
_data = inject(MAT_DIALOG_DATA);
|
|
13330
12387
|
_dialogRef = inject(MatDialogRef);
|
|
13331
12388
|
_reportData = inject(ReportData);
|
|
13332
12389
|
_message = inject(FsMessage);
|
|
13333
12390
|
_prompt = inject(FsPrompt);
|
|
13334
12391
|
_destroyRef = inject(DestroyRef);
|
|
12392
|
+
_injector = inject(Injector);
|
|
12393
|
+
_document = inject(DOCUMENT);
|
|
12394
|
+
// The id of the Label or Name input the server last refused, focused once
|
|
12395
|
+
// it is on screen (see focusRefusal).
|
|
12396
|
+
_refusedInputId = null;
|
|
13335
12397
|
// Only a server that resolves a report's locale offers the locale list.
|
|
13336
12398
|
_serverHasLocale = this._data.report.effectiveLocale !== undefined;
|
|
13337
12399
|
// True once the locale request has answered, with the list or an error.
|
|
@@ -13380,13 +12442,23 @@ class ReportSettingsComponent {
|
|
|
13380
12442
|
.subscribe();
|
|
13381
12443
|
}
|
|
13382
12444
|
}
|
|
13383
|
-
// A change to a row's Default period, exclude toggle or range ends. A new
|
|
12445
|
+
// A change to a row's Label, Name, Default period, exclude toggle or range ends. A new
|
|
13384
12446
|
// row object rather than a mutation, so the signal sees the change; the
|
|
13385
12447
|
// @for tracks group.id, so the row's controls survive the swap.
|
|
13386
12448
|
patchRow(row, patch) {
|
|
13387
12449
|
this.filterRows.update((rows) => rows
|
|
13388
12450
|
.map((item) => (item.group.id === row.group.id ? { ...item, ...patch } : item)));
|
|
13389
12451
|
}
|
|
12452
|
+
// Focuses the Label or Name input the server refused, once it is on
|
|
12453
|
+
// screen: straight after the refusal when the Filters tab is showing, else
|
|
12454
|
+
// when the tab group has brought it forward.
|
|
12455
|
+
focusRefusal() {
|
|
12456
|
+
const input = this._refusedInputId ? this._document.getElementById(this._refusedInputId) : null;
|
|
12457
|
+
if (input) {
|
|
12458
|
+
input.focus();
|
|
12459
|
+
this._refusedInputId = null;
|
|
12460
|
+
}
|
|
12461
|
+
}
|
|
13390
12462
|
// Confirm and delete here, while the dialog is still open, then close with
|
|
13391
12463
|
// a `deleted` result. The page reacts to the result; it no longer owns the
|
|
13392
12464
|
// prompt or the API call.
|
|
@@ -13403,10 +12475,9 @@ class ReportSettingsComponent {
|
|
|
13403
12475
|
}
|
|
13404
12476
|
save = () => {
|
|
13405
12477
|
const reportId = this._data.report.id;
|
|
13406
|
-
//
|
|
13407
|
-
// the single Save covers all three tabs. The report reloads on the `saved`
|
|
12478
|
+
// One Save covers all three tabs. The report reloads on the `saved`
|
|
13408
12479
|
// result, re-seeding the bar from the new defaults.
|
|
13409
|
-
const settings
|
|
12480
|
+
const settings = {
|
|
13410
12481
|
name: this.name,
|
|
13411
12482
|
pageWidth: this._pageDimension(this.pageWidth),
|
|
13412
12483
|
pageHeight: this._pageDimension(this.pageHeight),
|
|
@@ -13417,14 +12488,27 @@ class ReportSettingsComponent {
|
|
|
13417
12488
|
headerHeight: this._pageDimension(this.headerHeight) || 0,
|
|
13418
12489
|
footerHeight: this._pageDimension(this.footerHeight) || 0,
|
|
13419
12490
|
},
|
|
13420
|
-
}
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13426
|
-
|
|
13427
|
-
|
|
12491
|
+
};
|
|
12492
|
+
// One group update per changed filter, carrying only what changed: its
|
|
12493
|
+
// label, name and default all live in the group's config, so two requests
|
|
12494
|
+
// for one group would each write the config over the other.
|
|
12495
|
+
const saves = this.filterRows()
|
|
12496
|
+
.map((row) => ({ row, changes: this._groupChanges(row) }))
|
|
12497
|
+
.filter((save) => Object.keys(save.changes).length > 0);
|
|
12498
|
+
// A label or name can be refused, so those updates go first, one after
|
|
12499
|
+
// another, and a refusal stops the save before anything else is written:
|
|
12500
|
+
// the report settings and the other defaults wait until every name is
|
|
12501
|
+
// taken. In sequence, two renames in one save are checked against each
|
|
12502
|
+
// other, and a rename onto a name another filter is giving up goes after
|
|
12503
|
+
// it. The server applies name before default, so a refused name leaves
|
|
12504
|
+
// that filter's default unwritten too.
|
|
12505
|
+
const named = this._renameOrder(saves.filter((save) => this._namesOrLabels(save.changes)));
|
|
12506
|
+
const defaults = saves.filter((save) => !this._namesOrLabels(save.changes));
|
|
12507
|
+
return from(named)
|
|
12508
|
+
.pipe(concatMap((save) => this._saveNamed(reportId, save)), toArray(), switchMap(() => forkJoin([
|
|
12509
|
+
this._reportData.update(reportId, settings),
|
|
12510
|
+
...defaults.map((save) => this._reportData.updateFilterGroup(reportId, save.row.group.id, save.changes)),
|
|
12511
|
+
].map((request$) => request$.pipe(tap(() => this.wrote.set(true)))))), tap(() => {
|
|
13428
12512
|
this._message.success('Report settings saved');
|
|
13429
12513
|
this._dialogRef.close({ action: 'saved' });
|
|
13430
12514
|
}));
|
|
@@ -13454,6 +12538,72 @@ class ReportSettingsComponent {
|
|
|
13454
12538
|
_storedOption(stored, offered, label) {
|
|
13455
12539
|
return stored && !offered.includes(stored) ? [{ value: stored, label: label(stored) }] : [];
|
|
13456
12540
|
}
|
|
12541
|
+
// What a save changes on a row's group: the label, name and default, each
|
|
12542
|
+
// only when the operator changed it. Label and name are sent only to a
|
|
12543
|
+
// server that sent a name, and never one from the other.
|
|
12544
|
+
_groupChanges(row) {
|
|
12545
|
+
const changes = {};
|
|
12546
|
+
if (row.showName) {
|
|
12547
|
+
const label = row.label.trim();
|
|
12548
|
+
if (label !== (row.group.label ?? '')) {
|
|
12549
|
+
changes.label = label;
|
|
12550
|
+
}
|
|
12551
|
+
const name = row.name.trim();
|
|
12552
|
+
if (name !== row.group.name) {
|
|
12553
|
+
changes.name = name;
|
|
12554
|
+
}
|
|
12555
|
+
}
|
|
12556
|
+
const rowDefault = this._rowDefault(row);
|
|
12557
|
+
if (defaultChanged(row.group.config?.default, rowDefault)) {
|
|
12558
|
+
changes.default = rowDefault;
|
|
12559
|
+
}
|
|
12560
|
+
return changes;
|
|
12561
|
+
}
|
|
12562
|
+
_namesOrLabels(changes) {
|
|
12563
|
+
return changes.name !== undefined || changes.label !== undefined;
|
|
12564
|
+
}
|
|
12565
|
+
// The updates in the order they are sent: a rename onto a name another
|
|
12566
|
+
// filter holds goes after the others, so a filter giving that name up in
|
|
12567
|
+
// the same save has done so first.
|
|
12568
|
+
_renameOrder(saves) {
|
|
12569
|
+
const held = (save) => {
|
|
12570
|
+
const name = save.changes.name?.toLowerCase();
|
|
12571
|
+
return name !== undefined && this.filterRows()
|
|
12572
|
+
.some((row) => row.group.id !== save.row.group.id && row.group.name?.toLowerCase() === name);
|
|
12573
|
+
};
|
|
12574
|
+
return [...saves].sort((a, b) => Number(held(a)) - Number(held(b)));
|
|
12575
|
+
}
|
|
12576
|
+
// Saves a filter's update that carries a label or name. A refusal is the
|
|
12577
|
+
// server's to word ('Name "region" is already used by the filter
|
|
12578
|
+
// "Area".'), so it shows on the field rather than in a dialog, and the save
|
|
12579
|
+
// fails. What was saved becomes the row's stored group, so a later save
|
|
12580
|
+
// does not send it again.
|
|
12581
|
+
_saveNamed(reportId, save) {
|
|
12582
|
+
const { row, changes } = save;
|
|
12583
|
+
return this._reportData.updateFilterGroup(reportId, row.group.id, changes, { context: new HttpContext().set(DisplayApiError, false) })
|
|
12584
|
+
.pipe(tap(() => {
|
|
12585
|
+
this.wrote.set(true);
|
|
12586
|
+
this.patchRow(row, { group: storedGroup(row.group, changes) });
|
|
12587
|
+
}), catchError((error) => {
|
|
12588
|
+
this._showRefusal(row, changes, refusalMessage(error));
|
|
12589
|
+
return throwError(() => error);
|
|
12590
|
+
}));
|
|
12591
|
+
}
|
|
12592
|
+
// Shows a refusal on the field it is about (the name, which the server
|
|
12593
|
+
// checks first, else the label), brings the Filters tab forward and
|
|
12594
|
+
// focuses the field, so a save made from another tab says why it failed.
|
|
12595
|
+
_showRefusal(row, changes, message) {
|
|
12596
|
+
if (changes.name !== undefined) {
|
|
12597
|
+
this.patchRow(row, { nameRefusal: { value: changes.name, message } });
|
|
12598
|
+
this._refusedInputId = nameInputId(row.group.id);
|
|
12599
|
+
}
|
|
12600
|
+
else {
|
|
12601
|
+
this.patchRow(row, { labelRefusal: { value: changes.label ?? '', message } });
|
|
12602
|
+
this._refusedInputId = labelInputId(row.group.id);
|
|
12603
|
+
}
|
|
12604
|
+
this.selectedTab.set('filters');
|
|
12605
|
+
afterNextRender(() => this.focusRefusal(), { injector: this._injector });
|
|
12606
|
+
}
|
|
13457
12607
|
_throwBoundsError(min, max) {
|
|
13458
12608
|
const error = boundsError(parseDefaultBound(min), parseDefaultBound(max));
|
|
13459
12609
|
if (error) {
|
|
@@ -13499,7 +12649,12 @@ class ReportSettingsComponent {
|
|
|
13499
12649
|
const groupDefault = group.config?.default;
|
|
13500
12650
|
return {
|
|
13501
12651
|
group,
|
|
13502
|
-
|
|
12652
|
+
heading: defaultRowLabel(group, report.pages),
|
|
12653
|
+
label: group.label ?? '',
|
|
12654
|
+
name: group.name ?? '',
|
|
12655
|
+
showName: typeof group.name === 'string',
|
|
12656
|
+
labelRefusal: null,
|
|
12657
|
+
nameRefusal: null,
|
|
13503
12658
|
period: defaultPeriod(groupDefault),
|
|
13504
12659
|
periodOptions: defaultPeriodOptions(relativeLabel, groupDefault?.relative ?? null),
|
|
13505
12660
|
start: dateFromBound(groupDefault?.start),
|
|
@@ -13539,7 +12694,7 @@ class ReportSettingsComponent {
|
|
|
13539
12694
|
}
|
|
13540
12695
|
}
|
|
13541
12696
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13542
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <!-- One skeleton over the whole body until the locale list is in. -->\n <ng-container *fsSkeletonForm=\"ready() || null\">\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm.align-start\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n </div>\n <!--\n How deep the header and footer bands are. Their content is edited\n on the canvas (Edit layout \u2192 Header & footer); these reserve the\n space for it, and 0 means no band.\n -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Header height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"headerHeight\"\n name=\"headerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Footer height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"footerHeight\"\n name=\"footerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n <!--\n How numbers and dates are written (formatting only: no text is\n translated) and which currency money is labeled in. Hidden on a\n server too old to resolve a report's locale.\n -->\n @if (locales()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Locale\n </mat-label>\n <mat-select\n [ngModel]=\"locale()\"\n (ngModelChange)=\"locale.set($event)\"\n name=\"locale\">\n @for (option of localeOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n {{ formatExample() }}\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Currency\n </mat-label>\n <mat-select\n [ngModel]=\"currency()\"\n (ngModelChange)=\"currency.set($event)\"\n name=\"currency\">\n @for (option of currencyOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Names the currency amounts are in; it does not convert them.\n </mat-hint>\n </mat-form-field>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n <!-- One section per report bar filter: its heading, then its\n default, one field per row. -->\n @for (row of filterRows(); track row.group.id) {\n <h3>\n {{ row.label }}\n </h3>\n @switch (row.group.type) {\n @case ('dateRange') {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default period\n </mat-label>\n <mat-select\n [ngModel]=\"row.period\"\n (ngModelChange)=\"patchRow(row, { period: $event })\"\n [name]=\"'period' + row.group.id\">\n @for (option of row.periodOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (row.period === fixedDatesPeriod) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default from\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default to\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n }\n }\n @case ('select') {\n <fs-label-field class=\"full-width\">\n <fs-autocomplete-chips\n class=\"full-width\"\n label=\"Default values\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n </fs-label-field>\n @if (row.showExclude) {\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"row.exclude\"\n (ngModelChange)=\"patchRow(row, { exclude: $event })\"\n [name]=\"'exclude' + row.group.id\">\n Exclude these values\n </mat-slide-toggle>\n </fs-label-field>\n }\n }\n @case ('numberRange') {\n <!-- fsMaskNumber hands back text; the check and the save\n read it through parseDefaultBound, so 0 is kept. -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default minimum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.min\"\n (ngModelChange)=\"patchRow(row, { min: $event })\"\n [name]=\"'min' + row.group.id\"\n [validate]=\"minBoundValidator\"\n [validateData]=\"row.max\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default maximum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.max\"\n (ngModelChange)=\"patchRow(row, { max: $event })\"\n [name]=\"'max' + row.group.id\"\n [validate]=\"maxBoundValidator\"\n [validateData]=\"row.min\">\n </mat-form-field>\n }\n @default {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default text\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report bar filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i2$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "directive", type: i2$1.FsFormValidateDirective, selector: "[validate]", inputs: ["validate", "validateData", "validateOnSubmit"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i2$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "component", type: i5.FsLabelFieldComponent, selector: "fs-label-field", inputs: ["appearance", "showOutline", "disabled", "focused", "hoverable", "padless"] }, { kind: "ngmodule", type: FsMaskModule }, { kind: "directive", type: i3$2.FsMaskNumberDirective, selector: "[fsMaskNumber]" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i6$1.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i6.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i6.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4.FsSkeletonFormDirective, selector: "[fsSkeletonForm]", inputs: ["fsSkeletonForm", "fsSkeletonFormLines"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i9.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i7.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12697
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportSettingsComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <!-- One skeleton over the whole body until the locale list is in. -->\n <ng-container *fsSkeletonForm=\"ready() || null\">\n <!-- animationDone: a refused Label or Name is focused once the\n Filters tab is on screen. -->\n <mat-tab-group\n [(selected)]=\"selectedTab\"\n (animationDone)=\"focusRefusal()\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm.align-start\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n </div>\n <!--\n How deep the header and footer bands are. Their content is edited\n on the canvas (Edit layout \u2192 Header & footer); these reserve the\n space for it, and 0 means no band.\n -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Header height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"headerHeight\"\n name=\"headerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Footer height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"footerHeight\"\n name=\"footerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n <!--\n How numbers and dates are written (formatting only: no text is\n translated) and which currency money is labeled in. Hidden on a\n server too old to resolve a report's locale.\n -->\n @if (locales()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Locale\n </mat-label>\n <mat-select\n [ngModel]=\"locale()\"\n (ngModelChange)=\"locale.set($event)\"\n name=\"locale\">\n @for (option of localeOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n {{ formatExample() }}\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Currency\n </mat-label>\n <mat-select\n [ngModel]=\"currency()\"\n (ngModelChange)=\"currency.set($event)\"\n name=\"currency\">\n @for (option of currencyOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Names the currency amounts are in; it does not convert them.\n </mat-hint>\n </mat-form-field>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n <!-- One section per report bar filter: its heading, its label\n and name, then its default, one field per row. -->\n @for (row of filterRows(); track row.group.id) {\n <h3>\n {{ row.heading }}\n </h3>\n <!-- The label and the name are independent: a new label\n never changes the name, and the name is edited on its\n own. The server checks both, and its refusal shows on\n the field (refusalValidator). -->\n @if (row.showName) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Label\n </mat-label>\n <input\n matInput\n [id]=\"labelInputId(row.group.id)\"\n [ngModel]=\"row.label\"\n (ngModelChange)=\"patchRow(row, { label: $event })\"\n [name]=\"'label' + row.group.id\"\n [fsFormRequired]=\"true\"\n [fsFormMaxLength]=\"labelMaxLength\"\n [validate]=\"refusalValidator\"\n [validateData]=\"row.labelRefusal\">\n <mat-hint>\n What the filter shows. Changing it never changes the name.\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [id]=\"nameInputId(row.group.id)\"\n [ngModel]=\"row.name\"\n (ngModelChange)=\"patchRow(row, { name: $event })\"\n [name]=\"'name' + row.group.id\"\n [fsFormRequired]=\"true\"\n [fsFormPattern]=\"namePattern\"\n [fsFormPatternMessage]=\"namePatternMessage\"\n [fsFormMaxLength]=\"nameMaxLength\"\n [validate]=\"refusalValidator\"\n [validateData]=\"row.nameRefusal\">\n <mat-hint>\n How the AI refers to this filter: a letter, then letters and digits.\n </mat-hint>\n </mat-form-field>\n }\n @switch (row.group.type) {\n @case ('dateRange') {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default period\n </mat-label>\n <mat-select\n [ngModel]=\"row.period\"\n (ngModelChange)=\"patchRow(row, { period: $event })\"\n [name]=\"'period' + row.group.id\">\n @for (option of row.periodOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (row.period === fixedDatesPeriod) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default from\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default to\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n }\n }\n @case ('select') {\n <fs-label-field class=\"full-width\">\n <fs-autocomplete-chips\n class=\"full-width\"\n label=\"Default values\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n </fs-label-field>\n @if (row.showExclude) {\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"row.exclude\"\n (ngModelChange)=\"patchRow(row, { exclude: $event })\"\n [name]=\"'exclude' + row.group.id\">\n Exclude these values\n </mat-slide-toggle>\n </fs-label-field>\n }\n }\n @case ('numberRange') {\n <!-- fsMaskNumber hands back text; the check and the save\n read it through parseDefaultBound, so 0 is kept. -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default minimum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.min\"\n (ngModelChange)=\"patchRow(row, { min: $event })\"\n [name]=\"'min' + row.group.id\"\n [validate]=\"minBoundValidator\"\n [validateData]=\"row.max\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default maximum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.max\"\n (ngModelChange)=\"patchRow(row, { max: $event })\"\n [name]=\"'max' + row.group.id\"\n [validate]=\"maxBoundValidator\"\n [validateData]=\"row.min\">\n </mat-form-field>\n }\n @default {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default text\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report bar filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2$1.FsFormDirective, selector: "[fsForm]", inputs: ["wrapperSelector", "messageSelector", "hintSelector", "labelSelector", "autocomplete", "shortcuts", "confirm", "confirmDialog", "confirmDrawer", "confirmBrowser", "dirtySubmitButton", "submit", "successDelay", "errorDelay", "deactivationGuard"], outputs: ["fsForm", "invalid", "valid", "submitted", "reseted", "cleared"], exportAs: ["fsForm"] }, { kind: "directive", type: i2$1.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }, { kind: "directive", type: i2$1.FsFormMinDirective, selector: "[fsFormMin]", inputs: ["fsFormMin", "fsFormMinMessage"] }, { kind: "directive", type: i2$1.FsFormMaxDirective, selector: "[fsFormMax]", inputs: ["fsFormMax", "fsFormMaxMessage"] }, { kind: "directive", type: i2$1.FsFormMaxLengthDirective, selector: "[fsFormMaxLength]", inputs: ["fsFormMaxLength", "fsFormMaxLengthMessage"] }, { kind: "directive", type: i2$1.FsFormPatternDirective, selector: "[fsFormPattern]", inputs: ["fsFormPattern", "fsFormPatternMessage"] }, { kind: "directive", type: i2$1.FsFormValidateDirective, selector: "[validate]", inputs: ["validate", "validateData", "validateOnSubmit"] }, { kind: "component", type: i2$1.FsFormDialogActionsComponent, selector: "fs-form-dialog-actions", inputs: ["save", "create", "close", "done", "closeData", "name"] }, { kind: "directive", type: i2$1.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "directive", type: i2$1.FsButtonDirective, selector: "[mat-raised-button],[mat-button],[mat-flat-button],[mat-stroked-button]", inputs: ["name", "dirtySubmit"] }, { kind: "ngmodule", type: FsLabelModule }, { kind: "component", type: i5.FsLabelFieldComponent, selector: "fs-label-field", inputs: ["appearance", "showOutline", "disabled", "focused", "hoverable", "padless"] }, { kind: "ngmodule", type: FsMaskModule }, { kind: "directive", type: i3$2.FsMaskNumberDirective, selector: "[fsMaskNumber]" }, { kind: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "ngmodule", type: FsMessageModule }, { kind: "component", type: i6$1.FsMessageInfoComponent, selector: "fs-message-info" }, { kind: "ngmodule", type: FsTabsModule }, { kind: "directive", type: i6.FsTabsHeaderTabGroupDirective, selector: "mat-tab-group, matTabGroup, [matTabGroup]", inputs: ["orientation", "selected", "selectedData"], outputs: ["selectedChange", "selectedDataChange"], exportAs: ["fsTabsHeaderTabGroup"] }, { kind: "directive", type: i6.FsTabsTabDirective, selector: "mat-tab,matTab", inputs: ["name", "data"], exportAs: ["fsTabsTab"] }, { kind: "ngmodule", type: FsSkeletonModule }, { kind: "directive", type: i4.FsSkeletonFormDirective, selector: "[fsSkeletonForm]", inputs: ["fsSkeletonForm", "fsSkeletonFormLines"] }, { kind: "ngmodule", type: FsDatePickerModule }, { kind: "component", type: i9.FsDatePickerComponent, selector: "[fsDatePicker]", inputs: ["minYear", "maxYear", "minDate", "maxDate", "startOfDay", "view", "format", "minutes", "width"], outputs: ["change"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass"], exportAs: ["matTab"] }, { kind: "component", type: i7.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "component", type: MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "component", type: TimezoneSelectComponent, selector: "fs-ai-report-timezone-select", inputs: ["placeholder", "required", "disabled", "timezone"], outputs: ["timezoneChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13543
12698
|
}
|
|
13544
12699
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportSettingsComponent, decorators: [{
|
|
13545
12700
|
type: Component,
|
|
@@ -13568,36 +12723,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
13568
12723
|
MatOption,
|
|
13569
12724
|
MatSlideToggle,
|
|
13570
12725
|
TimezoneSelectComponent,
|
|
13571
|
-
], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <!-- One skeleton over the whole body until the locale list is in. -->\n <ng-container *fsSkeletonForm=\"ready() || null\">\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm.align-start\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n </div>\n <!--\n How deep the header and footer bands are. Their content is edited\n on the canvas (Edit layout \u2192 Header & footer); these reserve the\n space for it, and 0 means no band.\n -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Header height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"headerHeight\"\n name=\"headerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Footer height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"footerHeight\"\n name=\"footerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n <!--\n How numbers and dates are written (formatting only: no text is\n translated) and which currency money is labeled in. Hidden on a\n server too old to resolve a report's locale.\n -->\n @if (locales()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Locale\n </mat-label>\n <mat-select\n [ngModel]=\"locale()\"\n (ngModelChange)=\"locale.set($event)\"\n name=\"locale\">\n @for (option of localeOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n {{ formatExample() }}\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Currency\n </mat-label>\n <mat-select\n [ngModel]=\"currency()\"\n (ngModelChange)=\"currency.set($event)\"\n name=\"currency\">\n @for (option of currencyOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Names the currency amounts are in; it does not convert them.\n </mat-hint>\n </mat-form-field>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n <!-- One section per report bar filter: its heading, then its\n default, one field per row. -->\n @for (row of filterRows(); track row.group.id) {\n <h3>\n {{ row.label }}\n </h3>\n @switch (row.group.type) {\n @case ('dateRange') {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default period\n </mat-label>\n <mat-select\n [ngModel]=\"row.period\"\n (ngModelChange)=\"patchRow(row, { period: $event })\"\n [name]=\"'period' + row.group.id\">\n @for (option of row.periodOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (row.period === fixedDatesPeriod) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default from\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default to\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n }\n }\n @case ('select') {\n <fs-label-field class=\"full-width\">\n <fs-autocomplete-chips\n class=\"full-width\"\n label=\"Default values\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n </fs-label-field>\n @if (row.showExclude) {\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"row.exclude\"\n (ngModelChange)=\"patchRow(row, { exclude: $event })\"\n [name]=\"'exclude' + row.group.id\">\n Exclude these values\n </mat-slide-toggle>\n </fs-label-field>\n }\n }\n @case ('numberRange') {\n <!-- fsMaskNumber hands back text; the check and the save\n read it through parseDefaultBound, so 0 is kept. -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default minimum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.min\"\n (ngModelChange)=\"patchRow(row, { min: $event })\"\n [name]=\"'min' + row.group.id\"\n [validate]=\"minBoundValidator\"\n [validateData]=\"row.max\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default maximum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.max\"\n (ngModelChange)=\"patchRow(row, { max: $event })\"\n [name]=\"'max' + row.group.id\"\n [validate]=\"maxBoundValidator\"\n [validateData]=\"row.min\">\n </mat-form-field>\n }\n @default {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default text\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report bar filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"] }]
|
|
12726
|
+
], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <!-- One skeleton over the whole body until the locale list is in. -->\n <ng-container *fsSkeletonForm=\"ready() || null\">\n <!-- animationDone: a refused Label or Name is focused once the\n Filters tab is on screen. -->\n <mat-tab-group\n [(selected)]=\"selectedTab\"\n (animationDone)=\"focusRefusal()\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm.align-start\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n </div>\n <!--\n How deep the header and footer bands are. Their content is edited\n on the canvas (Edit layout \u2192 Header & footer); these reserve the\n space for it, and 0 means no band.\n -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Header height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"headerHeight\"\n name=\"headerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Footer height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"footerHeight\"\n name=\"footerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n <!--\n How numbers and dates are written (formatting only: no text is\n translated) and which currency money is labeled in. Hidden on a\n server too old to resolve a report's locale.\n -->\n @if (locales()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Locale\n </mat-label>\n <mat-select\n [ngModel]=\"locale()\"\n (ngModelChange)=\"locale.set($event)\"\n name=\"locale\">\n @for (option of localeOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n {{ formatExample() }}\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Currency\n </mat-label>\n <mat-select\n [ngModel]=\"currency()\"\n (ngModelChange)=\"currency.set($event)\"\n name=\"currency\">\n @for (option of currencyOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Names the currency amounts are in; it does not convert them.\n </mat-hint>\n </mat-form-field>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n <!-- One section per report bar filter: its heading, its label\n and name, then its default, one field per row. -->\n @for (row of filterRows(); track row.group.id) {\n <h3>\n {{ row.heading }}\n </h3>\n <!-- The label and the name are independent: a new label\n never changes the name, and the name is edited on its\n own. The server checks both, and its refusal shows on\n the field (refusalValidator). -->\n @if (row.showName) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Label\n </mat-label>\n <input\n matInput\n [id]=\"labelInputId(row.group.id)\"\n [ngModel]=\"row.label\"\n (ngModelChange)=\"patchRow(row, { label: $event })\"\n [name]=\"'label' + row.group.id\"\n [fsFormRequired]=\"true\"\n [fsFormMaxLength]=\"labelMaxLength\"\n [validate]=\"refusalValidator\"\n [validateData]=\"row.labelRefusal\">\n <mat-hint>\n What the filter shows. Changing it never changes the name.\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [id]=\"nameInputId(row.group.id)\"\n [ngModel]=\"row.name\"\n (ngModelChange)=\"patchRow(row, { name: $event })\"\n [name]=\"'name' + row.group.id\"\n [fsFormRequired]=\"true\"\n [fsFormPattern]=\"namePattern\"\n [fsFormPatternMessage]=\"namePatternMessage\"\n [fsFormMaxLength]=\"nameMaxLength\"\n [validate]=\"refusalValidator\"\n [validateData]=\"row.nameRefusal\">\n <mat-hint>\n How the AI refers to this filter: a letter, then letters and digits.\n </mat-hint>\n </mat-form-field>\n }\n @switch (row.group.type) {\n @case ('dateRange') {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default period\n </mat-label>\n <mat-select\n [ngModel]=\"row.period\"\n (ngModelChange)=\"patchRow(row, { period: $event })\"\n [name]=\"'period' + row.group.id\">\n @for (option of row.periodOptions; track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.name }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (row.period === fixedDatesPeriod) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default from\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default to\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n }\n }\n @case ('select') {\n <fs-label-field class=\"full-width\">\n <fs-autocomplete-chips\n class=\"full-width\"\n label=\"Default values\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n </fs-label-field>\n @if (row.showExclude) {\n <fs-label-field>\n <mat-slide-toggle\n [ngModel]=\"row.exclude\"\n (ngModelChange)=\"patchRow(row, { exclude: $event })\"\n [name]=\"'exclude' + row.group.id\">\n Exclude these values\n </mat-slide-toggle>\n </fs-label-field>\n }\n }\n @case ('numberRange') {\n <!-- fsMaskNumber hands back text; the check and the save\n read it through parseDefaultBound, so 0 is kept. -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default minimum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.min\"\n (ngModelChange)=\"patchRow(row, { min: $event })\"\n [name]=\"'min' + row.group.id\"\n [validate]=\"minBoundValidator\"\n [validateData]=\"row.max\">\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default maximum\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [signed]=\"true\"\n [scale]=\"boundScale\"\n [ngModel]=\"row.max\"\n (ngModelChange)=\"patchRow(row, { max: $event })\"\n [name]=\"'max' + row.group.id\"\n [validate]=\"maxBoundValidator\"\n [validateData]=\"row.min\">\n </mat-form-field>\n }\n @default {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Default text\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report bar filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"] }]
|
|
13572
12727
|
}] });
|
|
12728
|
+
// What a refused request says: the server's message, else the error's own,
|
|
12729
|
+
// else a plain fallback.
|
|
12730
|
+
function refusalMessage(error) {
|
|
12731
|
+
const failure = error;
|
|
12732
|
+
const message = failure?.error?.message ?? failure?.message;
|
|
12733
|
+
return typeof message === 'string' && message ? message : 'The filter could not be saved.';
|
|
12734
|
+
}
|
|
12735
|
+
// The group as the server now stores it, once an update carrying `changes`
|
|
12736
|
+
// is taken.
|
|
12737
|
+
function storedGroup(group, changes) {
|
|
12738
|
+
return {
|
|
12739
|
+
...group,
|
|
12740
|
+
...(changes.name !== undefined ? { name: changes.name } : {}),
|
|
12741
|
+
...(changes.label !== undefined ? { label: changes.label } : {}),
|
|
12742
|
+
config: changes.default !== undefined
|
|
12743
|
+
? { ...group.config, default: changes.default ?? undefined }
|
|
12744
|
+
: group.config,
|
|
12745
|
+
};
|
|
12746
|
+
}
|
|
12747
|
+
// The ids of a filter's Label and Name inputs, so a refused one can be
|
|
12748
|
+
// focused.
|
|
12749
|
+
function labelInputId(groupId) {
|
|
12750
|
+
return `filterLabel${groupId}`;
|
|
12751
|
+
}
|
|
12752
|
+
function nameInputId(groupId) {
|
|
12753
|
+
return `filterName${groupId}`;
|
|
12754
|
+
}
|
|
13573
12755
|
|
|
13574
12756
|
// The open report's saved views, per open report beside the state service:
|
|
13575
12757
|
// the list the viewer may see, the view that is open, the order a report
|
|
13576
|
-
// opens in
|
|
13577
|
-
//
|
|
13578
|
-
//
|
|
13579
|
-
//
|
|
13580
|
-
//
|
|
12758
|
+
// opens in and the calls that change a view. The viewer state stays
|
|
12759
|
+
// ReportFilterStateService's: this service puts a view on screen through
|
|
12760
|
+
// apply() and reads the screen through snapshot(), base and viewChanged.
|
|
12761
|
+
// Nothing here runs for a report whose server does not list 'views'
|
|
12762
|
+
// (serves()). A view is opened from the Views menu only: a report keeps no
|
|
12763
|
+
// state in the browser URL, so there is no link to a view.
|
|
13581
12764
|
//
|
|
13582
12765
|
// The order a report opens in (open(), on its first load only):
|
|
13583
|
-
// 1.
|
|
13584
|
-
//
|
|
13585
|
-
//
|
|
13586
|
-
// the viewer owns that view it is the open view, and the screen is
|
|
13587
|
-
// compared with it. The tiles wait for that one view only; the list loads
|
|
13588
|
-
// behind them. A link with rpv and no state (the owner's view was open on
|
|
13589
|
-
// the report's defaults) is a link for the owner only; for anyone else
|
|
13590
|
-
// it is no link.
|
|
13591
|
-
// 2. Else the viewer's default view.
|
|
13592
|
-
// 3. Else the report's authored defaults.
|
|
13593
|
-
// A page in the link beats a view's page.
|
|
13594
|
-
//
|
|
13595
|
-
// The link (urlParams): while a shared view is open, rv and what differs
|
|
13596
|
-
// from it; while one of the viewer's own views is open, or when the screen
|
|
13597
|
-
// went back to something a link cannot say on top of the shared view (see
|
|
13598
|
-
// viewConfigDiff), everything that differs from the report's defaults, and
|
|
13599
|
-
// rpv when the viewer owns the open view. Sharing the open view, or no
|
|
13600
|
-
// longer sharing it, writes the link again.
|
|
12766
|
+
// 1. The viewer's default view.
|
|
12767
|
+
// 2. Else the report's authored defaults.
|
|
12768
|
+
// A ?page= the report opened with beats the default view's page.
|
|
13601
12769
|
class ReportViewService {
|
|
13602
12770
|
// The views the viewer may see, names and flags only: its own first, then
|
|
13603
12771
|
// the ones others share. Empty until the list is read.
|
|
@@ -13618,42 +12786,24 @@ class ReportViewService {
|
|
|
13618
12786
|
_openView = signal(null);
|
|
13619
12787
|
// The report the views belong to.
|
|
13620
12788
|
_reportId = null;
|
|
13621
|
-
// The note that the screen is too long for a link shows once per report.
|
|
13622
|
-
_tooLongShown = false;
|
|
13623
12789
|
// Whether the open report's server keeps saved views.
|
|
13624
12790
|
serves(report) {
|
|
13625
12791
|
return !!report?.viewerFeatures?.includes('views');
|
|
13626
12792
|
}
|
|
13627
12793
|
// A report's first load, in the open order above: the state service is
|
|
13628
|
-
// set up for `report` (init)
|
|
13629
|
-
// the
|
|
13630
|
-
//
|
|
13631
|
-
//
|
|
13632
|
-
open(report,
|
|
12794
|
+
// set up for `report` (init) once the list is read, and the screen opened
|
|
12795
|
+
// on the viewer's default view, else on the report's defaults. `pageId` is
|
|
12796
|
+
// the ?page= the report opened with, which beats the view's page. Ends once
|
|
12797
|
+
// the screen is set.
|
|
12798
|
+
open(report, pageId) {
|
|
13633
12799
|
this._start(report);
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
const state = hasReportUrlState(params, report.filterGroups ?? []);
|
|
13637
|
-
if (!state && !link.personalViewId) {
|
|
13638
|
-
return this._openDefault(report, page);
|
|
13639
|
-
}
|
|
13640
|
-
const linkDiff = { ...paramsToViewConfig(params, report.filterGroups ?? []), ...page };
|
|
13641
|
-
const viewId = link.viewId ?? link.personalViewId;
|
|
13642
|
-
return (viewId ? this._readView(report.id, viewId) : of(null))
|
|
13643
|
-
.pipe(switchMap((view) => {
|
|
13644
|
-
// A link that names only a personal view (its screen was the
|
|
13645
|
-
// report's defaults) is the owner's link; for anyone else it says
|
|
13646
|
-
// nothing, and the report opens as it would with no link.
|
|
13647
|
-
if (!state && !view?.mine) {
|
|
13648
|
-
return this._openDefault(report, page);
|
|
13649
|
-
}
|
|
12800
|
+
return this._readViews(report)
|
|
12801
|
+
.pipe(tap((list) => {
|
|
13650
12802
|
this._filterState.init(report);
|
|
13651
|
-
|
|
13652
|
-
|
|
13653
|
-
|
|
13654
|
-
|
|
13655
|
-
return of(undefined);
|
|
13656
|
-
}));
|
|
12803
|
+
if (list?.myDefault) {
|
|
12804
|
+
this._openOn(list.myDefault, () => this._filterState.apply(list.myDefault.config, pageId));
|
|
12805
|
+
}
|
|
12806
|
+
}), map(() => undefined));
|
|
13657
12807
|
}
|
|
13658
12808
|
// A later load of the open report (a chat edit, a settings save): the
|
|
13659
12809
|
// screen and the open view stay, and the list is read again behind them so
|
|
@@ -13726,20 +12876,6 @@ class ReportViewService {
|
|
|
13726
12876
|
this._message.success('View deleted');
|
|
13727
12877
|
}));
|
|
13728
12878
|
}
|
|
13729
|
-
// The query params that make the URL carry the screen (see the class
|
|
13730
|
-
// comment), spread over the owned keys `current` carries so the ones the
|
|
13731
|
-
// screen no longer needs are removed. The host adds its page key.
|
|
13732
|
-
// A select's value is named by `name`, else by the name `current` carries
|
|
13733
|
-
// for it.
|
|
13734
|
-
urlParams(current, name = () => null) {
|
|
13735
|
-
const currentName = urlValueNames(current, this._reportGroups());
|
|
13736
|
-
const write = this._linkWrite((groupId, value) => name(groupId, value) ?? currentName(groupId, value));
|
|
13737
|
-
if (write.tooLong && !this._tooLongShown) {
|
|
13738
|
-
this._tooLongShown = true;
|
|
13739
|
-
this._message.warning(URL_TOO_LONG_MESSAGE);
|
|
13740
|
-
}
|
|
13741
|
-
return { ...reportParamsToClear(current, this._reportGroups()), ...write.params };
|
|
13742
|
-
}
|
|
13743
12879
|
// Starts over for another report: no list and no open view.
|
|
13744
12880
|
_start(report) {
|
|
13745
12881
|
if (report.id === this._reportId) {
|
|
@@ -13749,35 +12885,6 @@ class ReportViewService {
|
|
|
13749
12885
|
this._views.set([]);
|
|
13750
12886
|
this._canShare.set(false);
|
|
13751
12887
|
this._openView.set(null);
|
|
13752
|
-
this._tooLongShown = false;
|
|
13753
|
-
}
|
|
13754
|
-
// No link: the viewer's default view, else the report's defaults, once the
|
|
13755
|
-
// list is read.
|
|
13756
|
-
_openDefault(report, page) {
|
|
13757
|
-
return this._readViews(report)
|
|
13758
|
-
.pipe(tap((list) => {
|
|
13759
|
-
this._filterState.init(report);
|
|
13760
|
-
if (list?.myDefault) {
|
|
13761
|
-
this._openOn(list.myDefault, () => this._filterState.apply(list.myDefault.config, page));
|
|
13762
|
-
}
|
|
13763
|
-
}), map(() => undefined));
|
|
13764
|
-
}
|
|
13765
|
-
// A link with state: on the shared view rv names, else on the report's
|
|
13766
|
-
// defaults; rpv's view is the open view only when the viewer owns it.
|
|
13767
|
-
_openLink(link, view, linkDiff) {
|
|
13768
|
-
if (link.viewId && view) {
|
|
13769
|
-
this._openOn(view, () => this._filterState.apply(view.config, linkDiff));
|
|
13770
|
-
return;
|
|
13771
|
-
}
|
|
13772
|
-
if (!link.viewId && view?.mine) {
|
|
13773
|
-
this._openOn(view, () => this._filterState.apply(null, linkDiff, view.config));
|
|
13774
|
-
return;
|
|
13775
|
-
}
|
|
13776
|
-
this._openView.set(null);
|
|
13777
|
-
this._filterState.apply(null, linkDiff);
|
|
13778
|
-
if (link.viewId) {
|
|
13779
|
-
this._message.warning(VIEW_NOT_AVAILABLE_MESSAGE);
|
|
13780
|
-
}
|
|
13781
12888
|
}
|
|
13782
12889
|
// Makes `view` the open view, puts it on screen (`applyView`), and says
|
|
13783
12890
|
// how many of its settings no longer apply (and to save it, when the
|
|
@@ -13791,9 +12898,7 @@ class ReportViewService {
|
|
|
13791
12898
|
}
|
|
13792
12899
|
}
|
|
13793
12900
|
// Runs a change on the open view and takes its answer as the open view
|
|
13794
|
-
// and its row in the list.
|
|
13795
|
-
// whose it is) changes the key a link names it by, so the URL is written
|
|
13796
|
-
// again.
|
|
12901
|
+
// and its row in the list.
|
|
13797
12902
|
_changeOpen(change) {
|
|
13798
12903
|
const open = this._openView();
|
|
13799
12904
|
if (!open) {
|
|
@@ -13803,9 +12908,6 @@ class ReportViewService {
|
|
|
13803
12908
|
.pipe(tap((view) => {
|
|
13804
12909
|
this._openView.set(view);
|
|
13805
12910
|
this._putRow(view);
|
|
13806
|
-
if (view.shared !== open.shared || view.mine !== open.mine) {
|
|
13807
|
-
this._filterState.rewriteUrl();
|
|
13808
|
-
}
|
|
13809
12911
|
}));
|
|
13810
12912
|
}
|
|
13811
12913
|
// A view's row in the list, replaced or added. A new default clears the
|
|
@@ -13837,64 +12939,6 @@ class ReportViewService {
|
|
|
13837
12939
|
}
|
|
13838
12940
|
}));
|
|
13839
12941
|
}
|
|
13840
|
-
// A view a link names; null when the viewer cannot open it. The server
|
|
13841
|
-
// answers 'View not found' for one it cannot see, which must not move the
|
|
13842
|
-
// host to its not-found page.
|
|
13843
|
-
_readView(reportId, reportViewId) {
|
|
13844
|
-
return this._reportData.view(reportId, reportViewId, { context: new HttpContext().set(ProcessApiError, false) })
|
|
13845
|
-
.pipe(catchError(() => of(null)));
|
|
13846
|
-
}
|
|
13847
|
-
// The params the screen writes (see the class comment).
|
|
13848
|
-
_linkWrite(name) {
|
|
13849
|
-
const open = this._openView();
|
|
13850
|
-
const screen = this._filterState.snapshot();
|
|
13851
|
-
const authored = this._filterState.authoredConfig();
|
|
13852
|
-
const authoredSort = (componentId) => this._authoredSort(componentId);
|
|
13853
|
-
if (open?.shared) {
|
|
13854
|
-
// A group the view holds that the base let go of (its authored default
|
|
13855
|
-
// changed while the view was open, and the screen took the new one)
|
|
13856
|
-
// is still the view's value when a link opens the view, so it is
|
|
13857
|
-
// compared with that value and written when the screen differs.
|
|
13858
|
-
const base = this._filterState.base();
|
|
13859
|
-
const linkBase = { ...base, groups: { ...open.config?.groups, ...base.groups } };
|
|
13860
|
-
const diff = viewConfigDiff(linkBase, screen, authored, authoredSort);
|
|
13861
|
-
if (diff) {
|
|
13862
|
-
return viewConfigToParams(this._linkDays(diff), this._reportGroups(), { viewId: open.id }, name);
|
|
13863
|
-
}
|
|
13864
|
-
}
|
|
13865
|
-
const full = viewConfigDiff(authored, screen, authored, authoredSort) ?? screen;
|
|
13866
|
-
const link = open?.mine ? { personalViewId: open.id } : {};
|
|
13867
|
-
return viewConfigToParams(this._linkDays(full), this._reportGroups(), link, name);
|
|
13868
|
-
}
|
|
13869
|
-
// `config` with each rolling range (a relative name, which a view keeps) as
|
|
13870
|
-
// the days it gives today in the report's zone: a link carries days, never a
|
|
13871
|
-
// preset name. A name the report does not know is left as it is, and the
|
|
13872
|
-
// link leaves that group out.
|
|
13873
|
-
_linkDays(config) {
|
|
13874
|
-
if (!config.groups) {
|
|
13875
|
-
return config;
|
|
13876
|
-
}
|
|
13877
|
-
const groups = Object.entries(config.groups)
|
|
13878
|
-
.map(([groupId, value]) => [groupId, this._groupDays(value)]);
|
|
13879
|
-
return { ...config, groups: Object.fromEntries(groups) };
|
|
13880
|
-
}
|
|
13881
|
-
_groupDays(value) {
|
|
13882
|
-
const range = value?.relative ? this._filterState.resolveRelative(value.relative) : null;
|
|
13883
|
-
return range ? toStoredGroupValue('dateRange', { start: range.start, end: range.end }) : value;
|
|
13884
|
-
}
|
|
13885
|
-
// Every filter group of the open report: the link keys each by its label.
|
|
13886
|
-
_reportGroups() {
|
|
13887
|
-
return this._filterState.report()?.filterGroups ?? [];
|
|
13888
|
-
}
|
|
13889
|
-
// A list's authored first sort term, or null for any other component.
|
|
13890
|
-
_authoredSort(componentId) {
|
|
13891
|
-
const report = this._filterState.report();
|
|
13892
|
-
const component = [
|
|
13893
|
-
...(report?.pages ?? []).flatMap((page) => page.components ?? []),
|
|
13894
|
-
...(report?.master?.components ?? []),
|
|
13895
|
-
].find((candidate) => candidate.id === componentId);
|
|
13896
|
-
return component?.type === 'list' ? listSortTerms(component.config)[0] ?? null : null;
|
|
13897
|
-
}
|
|
13898
12942
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
13899
12943
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewService });
|
|
13900
12944
|
}
|
|
@@ -14484,7 +13528,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
14484
13528
|
// in the report's format context, as on screen.
|
|
14485
13529
|
async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
|
|
14486
13530
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
14487
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
13531
|
+
const echarts = (await import('./firestitch-report-echarts-DHEH_K_q.mjs')).default;
|
|
14488
13532
|
const host = document.createElement('div');
|
|
14489
13533
|
host.style.width = `${width}px`;
|
|
14490
13534
|
host.style.height = `${height}px`;
|
|
@@ -17114,7 +16158,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
17114
16158
|
// presets (Today, Last 7 days, This month, ...). The report adds none of its
|
|
17115
16159
|
// own: the built-in list covers every period a report filter names. A preset
|
|
17116
16160
|
// is a shortcut only: picking one sets the range's two days, which is all the
|
|
17117
|
-
// bar
|
|
16161
|
+
// bar and a saved view hold. Provided on the view; the host app's
|
|
17118
16162
|
// own pickers keep their config.
|
|
17119
16163
|
function provideReportDatePicker() {
|
|
17120
16164
|
const config = {
|
|
@@ -17279,11 +16323,12 @@ class ReportComponent {
|
|
|
17279
16323
|
if (!this.report()) {
|
|
17280
16324
|
return;
|
|
17281
16325
|
}
|
|
17282
|
-
this._dialog.open(ReportSettingsComponent, {
|
|
16326
|
+
const dialogRef = this._dialog.open(ReportSettingsComponent, {
|
|
17283
16327
|
data: { report: this.report() },
|
|
17284
16328
|
width: '460px',
|
|
17285
|
-
})
|
|
17286
|
-
|
|
16329
|
+
});
|
|
16330
|
+
const settings = dialogRef.componentInstance;
|
|
16331
|
+
dialogRef.afterClosed()
|
|
17287
16332
|
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
17288
16333
|
.subscribe((result) => {
|
|
17289
16334
|
// The dialog confirms + performs the delete itself; we just react to
|
|
@@ -17294,7 +16339,9 @@ class ReportComponent {
|
|
|
17294
16339
|
this._buildReportConfig();
|
|
17295
16340
|
return;
|
|
17296
16341
|
}
|
|
17297
|
-
|
|
16342
|
+
// A save that failed part way (a refused name) may still have stored
|
|
16343
|
+
// the filters before it, so a dismissed dialog that wrote reloads too.
|
|
16344
|
+
if ((result?.action === 'saved' || settings.wrote()) && this.selected()) {
|
|
17298
16345
|
this._loadReport(this.selected().id);
|
|
17299
16346
|
this._refreshReportName(this.selected().id);
|
|
17300
16347
|
}
|
|
@@ -17457,8 +16504,8 @@ class ReportComponent {
|
|
|
17457
16504
|
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
17458
16505
|
.subscribe((report) => {
|
|
17459
16506
|
this.report.set(report);
|
|
17460
|
-
//
|
|
17461
|
-
//
|
|
16507
|
+
// A report keeps no state in the URL: its filters live in the state
|
|
16508
|
+
// service for as long as the report is open.
|
|
17462
16509
|
this._filterState.init(report);
|
|
17463
16510
|
this._buildReportConfig();
|
|
17464
16511
|
});
|
|
@@ -17545,7 +16592,7 @@ class ReportComponent {
|
|
|
17545
16592
|
// The bar's values into the state: every group it shows, the grain and,
|
|
17546
16593
|
// when the bar has the control, the match.
|
|
17547
16594
|
_barChanged(query, hasMatch) {
|
|
17548
|
-
for (const { groupId, value } of groupValuesFromQuery(query, this._reportGroups()
|
|
16595
|
+
for (const { groupId, value } of groupValuesFromQuery(query, this._reportGroups())) {
|
|
17549
16596
|
this._filterState.setValue(groupId, value);
|
|
17550
16597
|
}
|
|
17551
16598
|
// Not a filter group — the grain is read straight off the query.
|
|
@@ -17595,7 +16642,7 @@ class ReportComponent {
|
|
|
17595
16642
|
ReportExportService,
|
|
17596
16643
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
17597
16644
|
// this route's chunk.
|
|
17598
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
16645
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DHEH_K_q.mjs').then((module) => module.default) }),
|
|
17599
16646
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
17600
16647
|
// presets; a preset only sets the days.
|
|
17601
16648
|
provideReportDatePicker(),
|
|
@@ -17616,7 +16663,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
17616
16663
|
ReportExportService,
|
|
17617
16664
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
17618
16665
|
// this route's chunk.
|
|
17619
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
16666
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DHEH_K_q.mjs').then((module) => module.default) }),
|
|
17620
16667
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
17621
16668
|
// presets; a preset only sets the days.
|
|
17622
16669
|
provideReportDatePicker(),
|
|
@@ -19122,32 +18169,29 @@ class FsAiReportComponent {
|
|
|
19122
18169
|
reportId = input(null);
|
|
19123
18170
|
// API base for the reports + chat endpoints; override to mount elsewhere.
|
|
19124
18171
|
basePath = input('reports');
|
|
19125
|
-
// The query param
|
|
19126
|
-
//
|
|
19127
|
-
//
|
|
18172
|
+
// The query param a report reads its opening page from (a link in a ticket
|
|
18173
|
+
// can name a page: ?page=7). It is read once, when a report opens, and
|
|
18174
|
+
// never written: a report keeps no state in the browser URL. Its filters,
|
|
18175
|
+
// the page on screen and the rest live in the report's state
|
|
18176
|
+
// (ReportFilterStateService), so a reload opens the report on its defaults,
|
|
18177
|
+
// or on the viewer's default view. A host that wants no page read sets this
|
|
18178
|
+
// to null.
|
|
19128
18179
|
pageQueryParam = input('page');
|
|
19129
|
-
// Whether the viewer's filters, group-by grain and list sorts live in the URL
|
|
19130
|
-
// (region=, excludeRegion=, unitsMin=, groupBy, s<componentId>; see
|
|
19131
|
-
// report-url-state.ts), so a refresh or a shared link opens the report as
|
|
19132
|
-
// it was seen. On by default; a host that mounts several reports on one
|
|
19133
|
-
// route, or keeps query keys of its own that a filter group's label or a
|
|
19134
|
-
// tile key (s<n>) could take, turns it off.
|
|
19135
|
-
urlState = input(true);
|
|
19136
18180
|
// The report was deleted from its settings dialog — the host routes back to
|
|
19137
18181
|
// the listing.
|
|
19138
18182
|
deleted = output();
|
|
19139
18183
|
// The report was renamed (settings, or the agent) — the host can refresh a
|
|
19140
18184
|
// breadcrumb / document title.
|
|
19141
18185
|
renamed = output();
|
|
19142
|
-
// The viewer moved to another page
|
|
19143
|
-
//
|
|
19144
|
-
// own reasons.
|
|
18186
|
+
// The viewer moved to another page — for hosts that track the open page for
|
|
18187
|
+
// their own reasons. Nothing is written to the URL.
|
|
19145
18188
|
pageChanged = output();
|
|
19146
18189
|
report = signal(null);
|
|
19147
18190
|
editMode = signal(false);
|
|
19148
|
-
// The page the canvas should open on
|
|
19149
|
-
//
|
|
19150
|
-
// is never fought by a
|
|
18191
|
+
// The page the canvas should open on: the ?page= the report opened with
|
|
18192
|
+
// (read once per report), or an opened view's page. Cleared as soon as the
|
|
18193
|
+
// canvas reports back, so a later in-app page change is never fought by a
|
|
18194
|
+
// stale value.
|
|
19151
18195
|
pageId = signal(null);
|
|
19152
18196
|
// The page the chat is bound to: the one on screen. A report conversation
|
|
19153
18197
|
// belongs to exactly one page, so this both scopes the conversation list and
|
|
@@ -19214,7 +18258,6 @@ class FsAiReportComponent {
|
|
|
19214
18258
|
_canvas;
|
|
19215
18259
|
_reportData = inject(ReportData);
|
|
19216
18260
|
_reportAiChatData = inject(ReportAiChatData);
|
|
19217
|
-
_router = inject(Router);
|
|
19218
18261
|
_route = inject(ActivatedRoute);
|
|
19219
18262
|
_filterState = inject(ReportFilterStateService);
|
|
19220
18263
|
_filterOptions = inject(ReportFilterOptionsService);
|
|
@@ -19225,29 +18268,16 @@ class FsAiReportComponent {
|
|
|
19225
18268
|
_dialog = inject(MatDialog);
|
|
19226
18269
|
_destroyRef = inject(DestroyRef);
|
|
19227
18270
|
_zone = inject(NgZone);
|
|
19228
|
-
// The report
|
|
19229
|
-
// report
|
|
19230
|
-
// in memory
|
|
19231
|
-
|
|
19232
|
-
// Whether the canvas has reported the open page for this report. Until it
|
|
19233
|
-
// has, the page param in the URL is the deep link still being honored, and
|
|
19234
|
-
// the URL writer leaves it alone.
|
|
19235
|
-
_pageReported = false;
|
|
19236
|
-
// The canvas moved to another page. Merged into the single URL writer.
|
|
19237
|
-
_pageWrite$ = new Subject();
|
|
18271
|
+
// The report that has been opened (its default view, else its defaults).
|
|
18272
|
+
// A report opens once; a reload after a chat or dialog edit keeps the state
|
|
18273
|
+
// in memory.
|
|
18274
|
+
_openedReportId = null;
|
|
19238
18275
|
// The state version the bar was last built for (reportFilterKey).
|
|
19239
18276
|
_barVersion = signal(0);
|
|
19240
18277
|
constructor() {
|
|
19241
18278
|
// Each chat turn carries what the viewer has on screen. Only the function
|
|
19242
18279
|
// is set here; it is called when a turn is sent.
|
|
19243
18280
|
this._reportAiChatData.turnData = () => ({ viewerState: this._chatViewerState() });
|
|
19244
|
-
// The ONE writer of this view's query params. Page moves and viewer-state
|
|
19245
|
-
// changes both funnel here, and auditTime(0) folds a burst (a bar re-sets
|
|
19246
|
-
// every group it shows) into a single write. Every write carries the whole
|
|
19247
|
-
// state, so a write that supersedes another loses nothing.
|
|
19248
|
-
merge(this._filterState.urlChanges(), this._pageWrite$)
|
|
19249
|
-
.pipe(auditTime(0), tap(() => this._writeUrl()), takeUntilDestroyed(this._destroyRef))
|
|
19250
|
-
.subscribe();
|
|
19251
18281
|
// Point the shared data gateway at the configured base first (before any
|
|
19252
18282
|
// load below reads it).
|
|
19253
18283
|
effect(() => {
|
|
@@ -19256,9 +18286,9 @@ class FsAiReportComponent {
|
|
|
19256
18286
|
// (Re)load whenever the host switches the report id.
|
|
19257
18287
|
effect(() => {
|
|
19258
18288
|
const reportId = this.reportId();
|
|
19259
|
-
//
|
|
19260
|
-
// than once at construction: the host may swap
|
|
19261
|
-
// and each arrival carries its own ?page=.
|
|
18289
|
+
// The page this report opens on, from the ?page= the URL already holds.
|
|
18290
|
+
// Read per report rather than once at construction: the host may swap
|
|
18291
|
+
// reports on the same route, and each arrival carries its own ?page=.
|
|
19262
18292
|
const pageId = this._pageIdFromUrl();
|
|
19263
18293
|
// Only the report id and the page param re-run this. The view state below
|
|
19264
18294
|
// is written here and read by the config build, so it stays untracked.
|
|
@@ -19268,14 +18298,13 @@ class FsAiReportComponent {
|
|
|
19268
18298
|
// until its canvas says which page it landed on.
|
|
19269
18299
|
this.chatPageId.set(null);
|
|
19270
18300
|
this.pageId.set(pageId);
|
|
19271
|
-
this._pageReported = false;
|
|
19272
18301
|
if (reportId != null) {
|
|
19273
18302
|
this._loadReport(reportId);
|
|
19274
18303
|
}
|
|
19275
18304
|
else {
|
|
19276
|
-
// Forget the
|
|
19277
|
-
//
|
|
19278
|
-
this.
|
|
18305
|
+
// Forget the opened report, so a host that comes back to it opens
|
|
18306
|
+
// it again.
|
|
18307
|
+
this._openedReportId = null;
|
|
19279
18308
|
this.report.set(null);
|
|
19280
18309
|
this._buildReportConfig();
|
|
19281
18310
|
}
|
|
@@ -19322,18 +18351,21 @@ class FsAiReportComponent {
|
|
|
19322
18351
|
return;
|
|
19323
18352
|
}
|
|
19324
18353
|
const reportId = report.id;
|
|
19325
|
-
this._dialog.open(ReportSettingsComponent, {
|
|
18354
|
+
const dialogRef = this._dialog.open(ReportSettingsComponent, {
|
|
19326
18355
|
data: { report },
|
|
19327
18356
|
width: '460px',
|
|
19328
|
-
})
|
|
19329
|
-
|
|
18357
|
+
});
|
|
18358
|
+
const settings = dialogRef.componentInstance;
|
|
18359
|
+
dialogRef.afterClosed()
|
|
19330
18360
|
.pipe(takeUntilDestroyed(this._destroyRef))
|
|
19331
18361
|
.subscribe((result) => {
|
|
19332
18362
|
if (result?.action === 'deleted') {
|
|
19333
18363
|
this.deleted.emit(reportId);
|
|
19334
18364
|
return;
|
|
19335
18365
|
}
|
|
19336
|
-
|
|
18366
|
+
// A save that failed part way (a refused name) may still have stored
|
|
18367
|
+
// the filters before it, so a dismissed dialog that wrote reloads too.
|
|
18368
|
+
if (result?.action === 'saved' || settings.wrote()) {
|
|
19337
18369
|
this._loadReport(reportId, true);
|
|
19338
18370
|
}
|
|
19339
18371
|
});
|
|
@@ -19382,17 +18414,15 @@ class FsAiReportComponent {
|
|
|
19382
18414
|
}
|
|
19383
18415
|
});
|
|
19384
18416
|
}
|
|
19385
|
-
// The canvas moved to another page
|
|
19386
|
-
//
|
|
18417
|
+
// The canvas moved to another page. The report-level filters stay as they
|
|
18418
|
+
// are (they live in the state service), and the URL is left alone.
|
|
19387
18419
|
onCanvasPageChanged(pageId) {
|
|
19388
|
-
// The
|
|
18420
|
+
// The asked-for page has been consumed; from here the canvas owns the page.
|
|
19389
18421
|
this.pageId.set(null);
|
|
19390
18422
|
this.chatPageId.set(pageId);
|
|
19391
|
-
this._pageReported = true;
|
|
19392
18423
|
// The open page is part of the screen a view saves.
|
|
19393
18424
|
this._filterState.setPage(pageId);
|
|
19394
18425
|
this.pageChanged.emit(pageId);
|
|
19395
|
-
this._pageWrite$.next();
|
|
19396
18426
|
// The bar shows the page groups of the page on screen.
|
|
19397
18427
|
if (hasPageGroup(this.report())) {
|
|
19398
18428
|
this._buildReportConfig();
|
|
@@ -19513,8 +18543,8 @@ class FsAiReportComponent {
|
|
|
19513
18543
|
const clamped = Math.min(0.8, Math.max(0.2, ratio));
|
|
19514
18544
|
this._chatPanel.nativeElement.style.flexBasis = `${clamped * 100}%`;
|
|
19515
18545
|
}
|
|
19516
|
-
// The page id
|
|
19517
|
-
// or the value is not a usable id.
|
|
18546
|
+
// The page id the URL holds right now (?page=, read only), or null when the
|
|
18547
|
+
// page param is off or the value is not a usable id.
|
|
19518
18548
|
_pageIdFromUrl() {
|
|
19519
18549
|
const param = this.pageQueryParam();
|
|
19520
18550
|
if (!param) {
|
|
@@ -19523,34 +18553,6 @@ class FsAiReportComponent {
|
|
|
19523
18553
|
const pageId = Number(this._route.snapshot.queryParamMap.get(param));
|
|
19524
18554
|
return Number.isFinite(pageId) && pageId > 0 ? pageId : null;
|
|
19525
18555
|
}
|
|
19526
|
-
// Writes the open page and the viewer state into the URL in one go. Only
|
|
19527
|
-
// the keys this view owns are touched ('merge'), and replaceUrl keeps the
|
|
19528
|
-
// writes out of the browser history: Back leaves the report rather than
|
|
19529
|
-
// walking every page and filter visited.
|
|
19530
|
-
_writeUrl() {
|
|
19531
|
-
const report = this.report();
|
|
19532
|
-
// A report still on screen while the host's next one loads must not write
|
|
19533
|
-
// its state over the new report's URL.
|
|
19534
|
-
if (!report || report.id !== this.reportId()) {
|
|
19535
|
-
return;
|
|
19536
|
-
}
|
|
19537
|
-
const queryParams = this.urlState()
|
|
19538
|
-
? this._urlParams(report)
|
|
19539
|
-
: {};
|
|
19540
|
-
const pageParam = this.pageQueryParam();
|
|
19541
|
-
if (pageParam && this._pageReported) {
|
|
19542
|
-
queryParams[pageParam] = this.chatPageId();
|
|
19543
|
-
}
|
|
19544
|
-
if (!Object.keys(queryParams).length) {
|
|
19545
|
-
return;
|
|
19546
|
-
}
|
|
19547
|
-
this._router.navigate([], {
|
|
19548
|
-
relativeTo: this._route,
|
|
19549
|
-
queryParams,
|
|
19550
|
-
queryParamsHandling: 'merge',
|
|
19551
|
-
replaceUrl: true,
|
|
19552
|
-
});
|
|
19553
|
-
}
|
|
19554
18556
|
_loadReport(reportId, emitRenamed = false) {
|
|
19555
18557
|
this._reportData.get(reportId)
|
|
19556
18558
|
.pipe(switchMap((report) => this._openReport(report).pipe(map(() => report))), takeUntilDestroyed(this._destroyRef))
|
|
@@ -19563,19 +18565,15 @@ class FsAiReportComponent {
|
|
|
19563
18565
|
}
|
|
19564
18566
|
});
|
|
19565
18567
|
}
|
|
19566
|
-
// Sets the viewer state up for a loaded report.
|
|
19567
|
-
//
|
|
19568
|
-
//
|
|
19569
|
-
//
|
|
19570
|
-
// may wait for that view; any other reads the URL as it always did.
|
|
18568
|
+
// Sets the viewer state up for a loaded report. On its first load a server
|
|
18569
|
+
// that keeps saved views opens it on the viewer's default view, else on the
|
|
18570
|
+
// defaults (which waits for the list of views); any other server opens it
|
|
18571
|
+
// on the defaults. Every later load keeps the state in memory.
|
|
19571
18572
|
_openReport(report) {
|
|
19572
|
-
const firstLoad = report.id !== this.
|
|
19573
|
-
this.
|
|
18573
|
+
const firstLoad = report.id !== this._openedReportId;
|
|
18574
|
+
this._openedReportId = report.id;
|
|
19574
18575
|
if (!this._views.serves(report)) {
|
|
19575
|
-
|
|
19576
|
-
? readReportUrlParams(this._route.snapshot.queryParamMap, report.filterGroups ?? [])
|
|
19577
|
-
: null;
|
|
19578
|
-
this._filterState.init(report, restore);
|
|
18576
|
+
this._filterState.init(report);
|
|
19579
18577
|
return of(undefined);
|
|
19580
18578
|
}
|
|
19581
18579
|
if (!firstLoad) {
|
|
@@ -19583,21 +18581,11 @@ class FsAiReportComponent {
|
|
|
19583
18581
|
this._views.refresh(report);
|
|
19584
18582
|
return of(undefined);
|
|
19585
18583
|
}
|
|
19586
|
-
return this._views.open(report, this.
|
|
19587
|
-
}
|
|
19588
|
-
//
|
|
19589
|
-
//
|
|
19590
|
-
|
|
19591
|
-
const current = this._route.snapshot.queryParamMap;
|
|
19592
|
-
// A select's picks are written as value:name, with the names the bars
|
|
19593
|
-
// have read.
|
|
19594
|
-
const name = (groupId, value) => this._filterOptions.label(groupId, value);
|
|
19595
|
-
return this._views.serves(report)
|
|
19596
|
-
? this._views.urlParams(current, name)
|
|
19597
|
-
: buildReportUrlParams(this._filterState.urlSnapshot(report), current, report.filterGroups ?? [], name);
|
|
19598
|
-
}
|
|
19599
|
-
// Moves the canvas to the page an opened view put on screen (a page in a
|
|
19600
|
-
// link already won inside apply()). A reset, or a reload, leaves the page.
|
|
18584
|
+
return this._views.open(report, this.pageId());
|
|
18585
|
+
}
|
|
18586
|
+
// Moves the canvas to the page an opened view put on screen (a ?page= the
|
|
18587
|
+
// report opened with already won inside apply()). A reset, or a reload,
|
|
18588
|
+
// leaves the page.
|
|
19601
18589
|
_openAppliedPage() {
|
|
19602
18590
|
const pageId = this._filterState.page();
|
|
19603
18591
|
if (pageId && pageId !== this.chatPageId()) {
|
|
@@ -19639,10 +18627,8 @@ class FsAiReportComponent {
|
|
|
19639
18627
|
}
|
|
19640
18628
|
this._barVersion.set(this._filterState.stateVersion());
|
|
19641
18629
|
this.reportFilterConfig.set({
|
|
19642
|
-
// fs-filter never touches the URL or persists filter state:
|
|
19643
|
-
//
|
|
19644
|
-
// rolling range as two dates. ReportFilterStateService owns the values,
|
|
19645
|
-
// and _writeUrl is the one writer of the URL.
|
|
18630
|
+
// fs-filter never touches the URL or persists filter state: a report
|
|
18631
|
+
// keeps no state in the URL. ReportFilterStateService owns the values.
|
|
19646
18632
|
queryParam: false,
|
|
19647
18633
|
persist: false,
|
|
19648
18634
|
items,
|
|
@@ -19662,7 +18648,7 @@ class FsAiReportComponent {
|
|
|
19662
18648
|
// The bar's values into the state: every group it shows, the grain and,
|
|
19663
18649
|
// when the bar has the control, the match.
|
|
19664
18650
|
_barChanged(query, hasMatch) {
|
|
19665
|
-
for (const { groupId, value } of groupValuesFromQuery(query, this._reportGroups()
|
|
18651
|
+
for (const { groupId, value } of groupValuesFromQuery(query, this._reportGroups())) {
|
|
19666
18652
|
this._filterState.setValue(groupId, value);
|
|
19667
18653
|
}
|
|
19668
18654
|
// Not a filter group — the grain is read straight off the query.
|
|
@@ -19687,14 +18673,13 @@ class FsAiReportComponent {
|
|
|
19687
18673
|
return matchFilterItem(this.report(), this._reportGroups(), this.chatPageId(), this._filterState.match());
|
|
19688
18674
|
}
|
|
19689
18675
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
19690
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportComponent, isStandalone: true, selector: "fs-ai-report", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: true, isRequired: false, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, pageQueryParam: { classPropertyName: "pageQueryParam", publicName: "pageQueryParam", isSignal: true, isRequired: false, transformFunction: null }
|
|
18676
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FsAiReportComponent, isStandalone: true, selector: "fs-ai-report", inputs: { reportId: { classPropertyName: "reportId", publicName: "reportId", isSignal: true, isRequired: false, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, pageQueryParam: { classPropertyName: "pageQueryParam", publicName: "pageQueryParam", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { deleted: "deleted", renamed: "renamed", pageChanged: "pageChanged" }, providers: [
|
|
19691
18677
|
// Session filter state is per-open-report, not app-global; the export
|
|
19692
18678
|
// services resolve filters through it, so they live at the same level.
|
|
19693
18679
|
ReportFilterStateService,
|
|
19694
18680
|
// The bars' select lists, narrowed by the other filters; per open report.
|
|
19695
18681
|
ReportFilterOptionsService,
|
|
19696
|
-
// The report's saved views
|
|
19697
|
-
// open report.
|
|
18682
|
+
// The report's saved views and the order it opens in; per open report.
|
|
19698
18683
|
ReportViewService,
|
|
19699
18684
|
ReportExportCollectorService,
|
|
19700
18685
|
ReportPptxService,
|
|
@@ -19706,7 +18691,7 @@ class FsAiReportComponent {
|
|
|
19706
18691
|
ReportAiChatData,
|
|
19707
18692
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
19708
18693
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
19709
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
18694
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DHEH_K_q.mjs').then((module) => module.default) }),
|
|
19710
18695
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
19711
18696
|
// presets; a preset only sets the days.
|
|
19712
18697
|
provideReportDatePicker(),
|
|
@@ -19720,8 +18705,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19720
18705
|
ReportFilterStateService,
|
|
19721
18706
|
// The bars' select lists, narrowed by the other filters; per open report.
|
|
19722
18707
|
ReportFilterOptionsService,
|
|
19723
|
-
// The report's saved views
|
|
19724
|
-
// open report.
|
|
18708
|
+
// The report's saved views and the order it opens in; per open report.
|
|
19725
18709
|
ReportViewService,
|
|
19726
18710
|
ReportExportCollectorService,
|
|
19727
18711
|
ReportPptxService,
|
|
@@ -19733,7 +18717,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19733
18717
|
ReportAiChatData,
|
|
19734
18718
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
19735
18719
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
19736
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
18720
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DHEH_K_q.mjs').then((module) => module.default) }),
|
|
19737
18721
|
// The bar's date pickers start their weeks on Monday and list the built-in
|
|
19738
18722
|
// presets; a preset only sets the days.
|
|
19739
18723
|
provideReportDatePicker(),
|
|
@@ -19771,4 +18755,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
19771
18755
|
*/
|
|
19772
18756
|
|
|
19773
18757
|
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 };
|
|
19774
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
18758
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-ZcHTnNKq.mjs.map
|