@firestitch/report 18.10.2 → 18.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/app/reports/components/report-canvas/report-canvas.component.d.ts +0 -1
  2. package/app/reports/data/report.data.d.ts +8 -12
  3. package/app/reports/export/report-export-collector.service.d.ts +0 -1
  4. package/app/reports/export/report-filter-lines.d.ts +0 -2
  5. package/app/reports/interfaces/report-chat-viewer-state.interface.d.ts +0 -2
  6. package/app/reports/interfaces/report-export-snapshot.interface.d.ts +2 -3
  7. package/app/reports/interfaces/report-saved-filter-changes.interface.d.ts +4 -0
  8. package/app/reports/interfaces/report-saved-filter.interface.d.ts +6 -0
  9. package/app/reports/interfaces/{report-view-config.interface.d.ts → report-state-snapshot.interface.d.ts} +1 -2
  10. package/app/reports/interfaces/report-viewer-feature.type.d.ts +1 -1
  11. package/app/reports/report-chat-viewer-state.d.ts +3 -3
  12. package/app/reports/report-saved-filters.d.ts +3 -0
  13. package/app/reports/report-state.d.ts +15 -0
  14. package/app/reports/services/report-filter-state.service.d.ts +9 -25
  15. package/app/reports/tile-data-state.d.ts +5 -5
  16. package/app/reports/views/report/fs-ai-report.component.d.ts +10 -5
  17. package/esm2022/app/reports/canvas-landing-page.mjs +3 -3
  18. package/esm2022/app/reports/components/component-list/component-list.component.mjs +3 -3
  19. package/esm2022/app/reports/components/report-canvas/report-canvas.component.mjs +8 -12
  20. package/esm2022/app/reports/components/report-component/report-component.component.mjs +16 -21
  21. package/esm2022/app/reports/data/report.data.mjs +24 -41
  22. package/esm2022/app/reports/export/report-export-collector.service.mjs +11 -18
  23. package/esm2022/app/reports/export/report-filter-lines.mjs +5 -15
  24. package/esm2022/app/reports/filter-settings-rules.mjs +2 -2
  25. package/esm2022/app/reports/interfaces/canvas-landing-input.interface.mjs +1 -1
  26. package/esm2022/app/reports/interfaces/report-chat-viewer-state.interface.mjs +1 -1
  27. package/esm2022/app/reports/interfaces/report-export-snapshot.interface.mjs +1 -1
  28. package/esm2022/app/reports/interfaces/report-saved-filter-changes.interface.mjs +2 -0
  29. package/esm2022/app/reports/interfaces/report-saved-filter.interface.mjs +2 -0
  30. package/esm2022/app/reports/interfaces/report-state-snapshot.interface.mjs +2 -0
  31. package/esm2022/app/reports/interfaces/report-temporary-state.interface.mjs +1 -1
  32. package/esm2022/app/reports/interfaces/report-tile-state.interface.mjs +1 -1
  33. package/esm2022/app/reports/interfaces/report-viewer-feature.type.mjs +1 -1
  34. package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
  35. package/esm2022/app/reports/mark-click.mjs +2 -2
  36. package/esm2022/app/reports/report-chat-viewer-state.mjs +3 -3
  37. package/esm2022/app/reports/report-date-presets.mjs +2 -2
  38. package/esm2022/app/reports/report-saved-filters.mjs +92 -0
  39. package/esm2022/app/reports/report-state.mjs +169 -0
  40. package/esm2022/app/reports/report-viewer-features.mjs +2 -4
  41. package/esm2022/app/reports/services/report-filter-state.service.mjs +83 -310
  42. package/esm2022/app/reports/tile-data-state.mjs +4 -4
  43. package/esm2022/app/reports/tile-loads.mjs +5 -6
  44. package/esm2022/app/reports/views/report/fs-ai-report.component.mjs +165 -94
  45. package/esm2022/app/reports/views/report/report.component.mjs +14 -16
  46. package/esm2022/public_api.mjs +4 -8
  47. package/fesm2022/{firestitch-report-echarts-CyHkFToO.mjs → firestitch-report-echarts-DKXlgu1D.mjs} +2 -2
  48. package/fesm2022/{firestitch-report-echarts-CyHkFToO.mjs.map → firestitch-report-echarts-DKXlgu1D.mjs.map} +1 -1
  49. package/fesm2022/{firestitch-report-firestitch-report-DftB7M1o.mjs → firestitch-report-firestitch-report-Bnv62suw.mjs} +445 -1434
  50. package/fesm2022/firestitch-report-firestitch-report-Bnv62suw.mjs.map +1 -0
  51. package/fesm2022/firestitch-report.mjs +1 -1
  52. package/package.json +1 -1
  53. package/public_api.d.ts +3 -7
  54. package/app/reports/components/report-view-picker/report-view-picker.component.d.ts +0 -21
  55. package/app/reports/dialogs/report-view/report-view-dialog.component.d.ts +0 -13
  56. package/app/reports/interfaces/report-view-create.interface.d.ts +0 -7
  57. package/app/reports/interfaces/report-view-deleted.interface.d.ts +0 -4
  58. package/app/reports/interfaces/report-view-dialog-data.interface.d.ts +0 -7
  59. package/app/reports/interfaces/report-view-dropped.interface.d.ts +0 -5
  60. package/app/reports/interfaces/report-view-menu-item.interface.d.ts +0 -10
  61. package/app/reports/interfaces/report-view-summary.interface.d.ts +0 -11
  62. package/app/reports/interfaces/report-view.interface.d.ts +0 -7
  63. package/app/reports/interfaces/report-views.interface.d.ts +0 -7
  64. package/app/reports/report-view-config.d.ts +0 -34
  65. package/app/reports/report-view-menu.d.ts +0 -15
  66. package/app/reports/services/report-view.service.d.ts +0 -39
  67. package/esm2022/app/reports/components/report-view-picker/report-view-picker.component.mjs +0 -115
  68. package/esm2022/app/reports/dialogs/report-view/report-view-dialog.component.mjs +0 -52
  69. package/esm2022/app/reports/interfaces/report-view-config.interface.mjs +0 -2
  70. package/esm2022/app/reports/interfaces/report-view-create.interface.mjs +0 -2
  71. package/esm2022/app/reports/interfaces/report-view-deleted.interface.mjs +0 -2
  72. package/esm2022/app/reports/interfaces/report-view-dialog-data.interface.mjs +0 -2
  73. package/esm2022/app/reports/interfaces/report-view-dropped.interface.mjs +0 -2
  74. package/esm2022/app/reports/interfaces/report-view-menu-item.interface.mjs +0 -2
  75. package/esm2022/app/reports/interfaces/report-view-summary.interface.mjs +0 -2
  76. package/esm2022/app/reports/interfaces/report-view.interface.mjs +0 -2
  77. package/esm2022/app/reports/interfaces/report-views.interface.mjs +0 -2
  78. package/esm2022/app/reports/report-view-config.mjs +0 -587
  79. package/esm2022/app/reports/report-view-menu.mjs +0 -119
  80. package/esm2022/app/reports/services/report-view.service.mjs +0 -218
  81. package/fesm2022/firestitch-report-firestitch-report-DftB7M1o.mjs.map +0 -1
@@ -12,9 +12,9 @@ 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 { ItemType, getExcludeName, FsFilterModule } from '@firestitch/filter';
15
+ import { ItemType, getExcludeName, FsFilterModule, FilterComponent } from '@firestitch/filter';
16
16
  import * as i2 from '@firestitch/menu';
17
- import { FsMenuModule, FsMenuComponent } from '@firestitch/menu';
17
+ import { FsMenuModule } from '@firestitch/menu';
18
18
  import { FsProcess } from '@firestitch/process';
19
19
  import { FsPrompt } from '@firestitch/prompt';
20
20
  import { map, shareReplay, catchError, scan, filter, take, tap, switchMap, finalize, concatMap, toArray, mergeMap as mergeMap$1, takeUntil } from 'rxjs/operators';
@@ -78,8 +78,8 @@ import { MatExpansionModule } from '@angular/material/expansion';
78
78
  // - The same report with more pages than before: a page was just added in
79
79
  // this tab, so the canvas lands on the last page. The asked-for page does
80
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.
81
+ // - Otherwise the asked-for page (a ?page= the report opened with), when the
82
+ // report has it.
83
83
  // - Another report that asks for no page it has opens on its first page:
84
84
  // the old report's page number means nothing there.
85
85
  // - The same report keeps the page it shows when the asked-for page does not
@@ -111,8 +111,7 @@ function canvasLandingPage(input) {
111
111
  // numberRange its min/max control, matchAny the report bar's Match select,
112
112
  // and pageScope shows a page's groups in the report bar of that page.
113
113
  // cascadingOptions narrows a select's list by the other filters of its tile
114
- // (ReportFilterOptionsService). views is the Views picker in the report's
115
- // header (ReportViewPickerComponent). drill is a time chart's click down, with
114
+ // (ReportFilterOptionsService). drill is a time chart's click down, with
116
115
  // the tile header's breadcrumb and its menu's Drill up and Back to top as the
117
116
  // way back.
118
117
  const VIEWER_FEATURES = [
@@ -122,7 +121,6 @@ const VIEWER_FEATURES = [
122
121
  'matchAny',
123
122
  'pageScope',
124
123
  'cascadingOptions',
125
- 'views',
126
124
  'drill',
127
125
  ];
128
126
  // The features a report offers on this screen: the ones its server serves
@@ -388,14 +386,11 @@ class ReportData {
388
386
  // filterSummary too, for a server that does not read filterLines; the
389
387
  // lines are left out when there are none. `states` is each tile's own
390
388
  // state by component id (zipExportStates), so each file holds what its
391
- // tile shows; it is left out when no tile has any. `viewId` names the open
392
- // saved view, which the server names in the notes, and `viewChanged` says
393
- // whether the screen differs from it; both are left out with no view open.
394
- // `titles` holds each component's title as the viewer reads it, its tokens
389
+ // tile shows; it is left out when no tile has any. `titles` holds each component's title as the viewer reads it, its tokens
395
390
  // filled, by component id, which the server names the component's file
396
391
  // after; left out when there are none. Same keep-alive contract as
397
392
  // exportComponent, over a longer run; consume through FsProcess.download.
398
- exportReport(reportId, filters = [], pageId = null, frequency = null, filterLines = [], states = {}, viewId = null, viewChanged = false, titles = {}, config = {}) {
393
+ exportReport(reportId, filters = [], pageId = null, frequency = null, filterLines = [], states = {}, titles = {}, config = {}) {
399
394
  return this._api.post(this._path(`${reportId}/export`), {
400
395
  filters,
401
396
  ...(pageId ? { pageId } : {}),
@@ -403,7 +398,6 @@ class ReportData {
403
398
  filterSummary: filterLines.map((line) => line.text),
404
399
  ...(filterLines.length ? { filterLines } : {}),
405
400
  ...(Object.keys(states).length ? { states } : {}),
406
- ...(viewId ? { viewId, viewChanged } : {}),
407
401
  ...(Object.keys(titles).length ? { titles } : {}),
408
402
  }, {
409
403
  key: 'url',
@@ -563,52 +557,39 @@ class ReportData {
563
557
  ...config,
564
558
  });
565
559
  }
566
- // The report's saved views the viewer may see: names and flags, whether it
567
- // may share, and its own default view in full. No `key`: the whole payload
568
- // is the answer, as tree() reads it. Only for a report whose server lists
569
- // 'views' (Report.viewerFeatures).
570
- views(reportId, config = {}) {
571
- return this._api.get(this._path(`${reportId}/views`), {}, config);
572
- }
573
- // One saved view with its config, pruned to the report as it is now.
574
- view(reportId, reportViewId, config = {}) {
575
- return this._api.get(this._path(`${reportId}/views/${reportViewId}`), {}, {
576
- key: 'view',
560
+ // The report's saved filters, in their order. Shared by everyone who may
561
+ // open the report.
562
+ savedFilters(reportId, config = {}) {
563
+ return this._api.get(this._path(`${reportId}/savedfilters`), {}, {
564
+ key: 'savedFilters',
577
565
  ...config,
578
566
  });
579
567
  }
580
- createView(reportId, view, config = {}) {
581
- return this._api.post(this._path(`${reportId}/views`), view, {
582
- key: 'view',
568
+ // A new saved filter: its name and the report-wide filter values it holds.
569
+ createSavedFilter(reportId, savedFilter, config = {}) {
570
+ return this._api.post(this._path(`${reportId}/savedfilters`), savedFilter, {
571
+ key: 'savedFilter',
583
572
  ...config,
584
573
  });
585
574
  }
586
- // Renames a view or replaces its config; only the keys given change.
587
- updateView(reportId, reportViewId, changes, config = {}) {
588
- return this._api.put(this._path(`${reportId}/views/${reportViewId}`), changes, {
589
- key: 'view',
575
+ // Renames a saved filter or replaces its values; only the keys given change.
576
+ updateSavedFilter(reportId, reportSavedFilterId, changes, config = {}) {
577
+ return this._api.put(this._path(`${reportId}/savedfilters/${reportSavedFilterId}`), changes, {
578
+ key: 'savedFilter',
590
579
  ...config,
591
580
  });
592
581
  }
593
- // Resolves to the deleted view's id and its state.
594
- deleteView(reportId, reportViewId, config = {}) {
595
- return this._api.delete(this._path(`${reportId}/views/${reportViewId}`), {}, {
596
- key: 'view',
582
+ deleteSavedFilter(reportId, reportSavedFilterId, config = {}) {
583
+ return this._api.delete(this._path(`${reportId}/savedfilters/${reportSavedFilterId}`), {}, {
584
+ key: 'savedFilter',
597
585
  ...config,
598
586
  });
599
587
  }
600
- // Shares a view with everyone who may open the report, or stops sharing it.
601
- shareView(reportId, reportViewId, shared, config = {}) {
602
- return this._api.post(this._path(`${reportId}/views/${reportViewId}/share`), { shared }, {
603
- key: 'view',
604
- ...config,
605
- });
606
- }
607
- // Makes a view the viewer's default, or stops opening it by default. The
608
- // server clears the viewer's previous default.
609
- setViewDefault(reportId, reportViewId, personalDefault, config = {}) {
610
- return this._api.post(this._path(`${reportId}/views/${reportViewId}/default`), { personalDefault }, {
611
- key: 'view',
588
+ // Persist the saved filters' order after a drag: every id, in its new
589
+ // order. Resolves to the list in that order.
590
+ saveSavedFilterPositions(reportId, reportSavedFilterIds, config = {}) {
591
+ return this._api.put(this._path(`${reportId}/savedfilters/positions`), { reportSavedFilterIds }, {
592
+ key: 'savedFilters',
612
593
  ...config,
613
594
  });
614
595
  }
@@ -4966,13 +4947,7 @@ function isTile(component) {
4966
4947
  return !!component && TILE_TYPES.includes(component.type);
4967
4948
  }
4968
4949
 
4969
- // The version this viewer writes and reads.
4970
- const REPORT_VIEW_CONFIG_VERSION = 1;
4971
- // The caps of a view document, the server's.
4972
- const VIEW_CONFIG_MAX_BYTES = 32 * 1024;
4973
- const VIEW_CONFIG_MAX_GROUPS = 100;
4974
- const VIEW_CONFIG_MAX_TILES = 200;
4975
- const GROUP_VALUE_MAX_PICKS = 500;
4950
+ // The server's cap on a drill path.
4976
4951
  const DRILL_MAX_DEPTH = 5;
4977
4952
  // Every relative range name the server knows, in its enum's order. A record
4978
4953
  // rather than a list, so the build fails when ReportRelativeRange gains a
@@ -4993,21 +4968,9 @@ const RELATIVE_RANGES = {
4993
4968
  // The same names as a list, in the server enum's order (the record's key
4994
4969
  // order): the order the date picker offers them in.
4995
4970
  const RELATIVE_RANGE_NAMES = Object.keys(RELATIVE_RANGES);
4996
- // The keys a group value may hold, by its group's type.
4997
- const GROUP_VALUE_KEYS = {
4998
- dateRange: ['relative', 'start', 'end'],
4999
- select: ['values', 'excludeValues'],
5000
- keyword: ['value'],
5001
- numberRange: ['min', 'max'],
5002
- };
5003
- // The keys of a view document and of a tile state this viewer reads. Any
5004
- // other key is kept as it was.
5005
- const VIEW_CONFIG_KEYS = ['v', 'pageId', 'groupBy', 'match', 'groups', 'tiles'];
5006
- const TILE_STATE_KEYS = ['sort', 'search', 'hidden', 'drill'];
5007
4971
  const DAY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
5008
4972
  // A number as text: what a masked input sends.
5009
4973
  const NUMBER_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
5010
- const ID_KEY_PATTERN = /^[1-9]\d*$/;
5011
4974
  // Whether a name is a relative range the server resolves.
5012
4975
  function isRelativeRange(name) {
5013
4976
  return typeof name === 'string' && Object.prototype.hasOwnProperty.call(RELATIVE_RANGES, name);
@@ -5031,65 +4994,9 @@ function numberFromText(text) {
5031
4994
  const number = NUMBER_PATTERN.test(text) ? Number(text) : NaN;
5032
4995
  return Number.isFinite(number) ? number : null;
5033
4996
  }
5034
- // A group value read for a group of `type`, on its way to being stored: only
5035
- // the parts that hold something. Throws, naming what is wrong, when a key
5036
- // does not belong to the type or a part breaks its type's rule.
5037
- function groupValueFromArray(value, type) {
5038
- requireGroupKeys(value, type);
5039
- switch (type) {
5040
- case 'dateRange':
5041
- return dateRangeValue(value);
5042
- case 'select':
5043
- return selectValue(value);
5044
- case 'numberRange':
5045
- return numberRangeValue(value);
5046
- default:
5047
- return keywordValue(value);
5048
- }
5049
- }
5050
- // A group value read back (a saved view) for a group of `type`: what
5051
- // groupValueFromArray gives, or null when it would refuse it.
5052
- function groupValueFromStored(value, type) {
5053
- if (!isPlainObject(value)) {
5054
- return null;
5055
- }
5056
- try {
5057
- return groupValueFromArray(value, type);
5058
- }
5059
- catch {
5060
- return null;
5061
- }
5062
- }
5063
- // A view document read for storing or applying. Throws when it is not a
5064
- // version 1 document, breaks a cap or holds a malformed known key.
5065
- function viewConfigFromArray(input) {
5066
- const doc = requireObject(input, 'A view config must be an object.');
5067
- requireSize(doc);
5068
- requireVersion(doc.v);
5069
- const config = { v: REPORT_VIEW_CONFIG_VERSION, ...otherKeys(doc, VIEW_CONFIG_KEYS) };
5070
- const pageId = optionalId(doc.pageId, 'pageId must be a page id.');
5071
- if (pageId !== null) {
5072
- config.pageId = pageId;
5073
- }
5074
- if (isSet(doc.groupBy)) {
5075
- config.groupBy = requireGranularity(doc.groupBy, 'groupBy must be a time grain.');
5076
- }
5077
- if (isSet(doc.match)) {
5078
- config.match = requireMatch(doc.match);
5079
- }
5080
- if (isSet(doc.groups)) {
5081
- config.groups = viewGroups(doc.groups);
5082
- }
5083
- if (isSet(doc.tiles)) {
5084
- config.tiles = viewTiles(doc.tiles);
5085
- }
5086
- return config;
5087
- }
5088
- // A group's value as a view stores it, read for the keys its
5089
- // group's type owns, or null when it holds nothing. A date range that still
5090
- // carries its relative preset is stored as that preset whatever today is: the
5091
- // viewer did not edit it, so a view saved after midnight with the page still
5092
- // open keeps rolling.
4997
+ // A group's value in its stored shape, read for the keys its group's type
4998
+ // owns, or null when it holds nothing. A date range that still carries its
4999
+ // relative preset is written as that preset whatever today is.
5093
5000
  function toStoredGroupValue(type, value) {
5094
5001
  if (!value) {
5095
5002
  return null;
@@ -5109,39 +5016,20 @@ function toStoredGroupValue(type, value) {
5109
5016
  return storedOrNull({ value: value.value?.trim() || undefined });
5110
5017
  }
5111
5018
  }
5112
- // A group value in its stored shape as the value the bars and the requests
5113
- // read: a relative preset as today's days in the report's zone, carrying its
5114
- // name; fixed days as dates; any other value as it is. Null when it holds
5115
- // nothing, or names a preset `resolveRelative` does not know.
5116
- function toLiveGroupValue(type, stored, resolveRelative) {
5117
- if (!stored || holdsNothing(stored)) {
5118
- return null;
5119
- }
5120
- if (type !== 'dateRange') {
5121
- return { ...stored };
5122
- }
5123
- if (stored.relative) {
5124
- const range = resolveRelative(stored.relative);
5125
- return range ? { start: range.start, end: range.end, preset: stored.relative } : null;
5126
- }
5127
- return { start: dateFromBound(stored.start), end: dateFromBound(stored.end) };
5128
- }
5129
- // Whether the screen's state differs from the view it opened on. `current`
5130
- // is a snapshot, so it holds every group the report has; a group only `base`
5131
- // holds is one the report no longer has, and is not compared. A group `base`
5132
- // does not hold was added after the view was saved: it is compared with its
5133
- // value in `authored` (the report's defaults), so leaving it at its default
5134
- // changes nothing. The page is not compared (moving to another page does not
5135
- // edit a view), and neither are keys this viewer does not know. Picks are
5019
+ // Whether the screen's state (`current`, a snapshot) differs from the
5020
+ // report's authored defaults (`defaults`): the match, the grain, a group's
5021
+ // value, or a tile's sort, search, hidden columns or drill path. The page is
5022
+ // not compared (moving to another page changes no filter). Picks are
5136
5023
  // compared in any order.
5137
- function viewDiffers(base, current, authored = null) {
5138
- return (base.match ?? 'all') !== (current.match ?? 'all')
5139
- || (base.groupBy ?? null) !== (current.groupBy ?? null)
5140
- || groupsDiffer(base, current, authored)
5141
- || tilesDiffer(base.tiles ?? {}, current.tiles ?? {});
5142
- }
5143
- // Whether two stored group values are the same filter, as viewDiffers reads
5144
- // a group: picks in any order, text trimmed, an empty value read as none.
5024
+ function stateDiffers(defaults, current) {
5025
+ return (defaults.match ?? 'all') !== (current.match ?? 'all')
5026
+ || (defaults.groupBy ?? null) !== (current.groupBy ?? null)
5027
+ || Object.entries(current.groups ?? {})
5028
+ .some(([groupId, value]) => storedValueKey(defaults.groups?.[Number(groupId)]) !== storedValueKey(value))
5029
+ || tilesDiffer(defaults.tiles ?? {}, current.tiles ?? {});
5030
+ }
5031
+ // Whether two stored group values are the same filter: picks in any order,
5032
+ // text trimmed, an empty value read as none.
5145
5033
  function sameStoredGroupValue(a, b) {
5146
5034
  return storedValueKey(a) === storedValueKey(b);
5147
5035
  }
@@ -5151,7 +5039,7 @@ function sameStoredGroupValue(a, b) {
5151
5039
  function cleanDrillPath(path) {
5152
5040
  const kept = [];
5153
5041
  for (const step of path ?? []) {
5154
- if (!isPlainObject(step) || !isChartGranularity(step.level) || !isDrillValue(step.value)
5042
+ if (!isPlainObject$1(step) || !isChartGranularity(step.level) || !isDrillValue(step.value)
5155
5043
  || kept.length === DRILL_MAX_DEPTH) {
5156
5044
  break;
5157
5045
  }
@@ -5164,83 +5052,16 @@ function cleanDrillPath(path) {
5164
5052
  function sameDrillPath(a, b) {
5165
5053
  return drillKey(a) === drillKey(b);
5166
5054
  }
5167
- // A view's config to save: the screen (`screen`, a snapshot), with the keys
5168
- // this viewer does not know kept from the stored document (`stored`), at the
5169
- // top and in each tile. They are never read here, and saving must not lose
5170
- // them.
5171
- function withStoredKeys(stored, screen) {
5172
- if (!stored) {
5173
- return screen;
5174
- }
5175
- const tiles = { ...screen.tiles };
5176
- Object.entries(stored.tiles ?? {}).forEach(([componentId, tile]) => {
5177
- const kept = isPlainObject(tile) ? otherKeys(tile, TILE_STATE_KEYS) : {};
5178
- if (Object.keys(kept).length) {
5179
- tiles[Number(componentId)] = { ...kept, ...tiles[Number(componentId)] };
5180
- }
5181
- });
5182
- const config = { ...otherKeys(stored, VIEW_CONFIG_KEYS), ...screen };
5183
- delete config.tiles;
5184
- return Object.keys(tiles).length ? { ...config, tiles } : config;
5185
- }
5186
- // How many settings of a saved view no longer apply because the report
5187
- // changed: what the server left out when it read the view (`dropped`), and
5188
- // what the viewer could not apply (`applied` is the view as it applied, the
5189
- // state service's base): a group value that no longer fits its group, and a
5190
- // list sort, search or hidden columns, or a chart's drill path, its tile can
5191
- // no longer take (a list column gone, a chart turned into a list).
5192
- function viewDroppedCount(view, applied) {
5193
- const dropped = view.dropped;
5194
- const groups = Object.entries(view.config?.groups ?? {})
5195
- .filter(([groupId, value]) => value !== null && !Object.prototype.hasOwnProperty.call(applied.groups ?? {}, groupId))
5196
- .length;
5197
- const tiles = Object.entries(view.config?.tiles ?? {})
5198
- .reduce((count, [componentId, tile]) => count + tileKeysLost(tile, applied.tiles?.[Number(componentId)]), 0);
5199
- return (dropped?.groups?.length ?? 0) + (dropped?.tiles?.length ?? 0) + (dropped?.page ? 1 : 0) + groups + tiles;
5200
- }
5201
- // The note a view opens with when some of its settings no longer apply, or
5202
- // null when every setting applies. Only a viewer who may change the view
5203
- // (`canChange`) is told to save it.
5204
- function viewDroppedMessage(count, canChange = true) {
5205
- if (count < 1) {
5206
- return null;
5207
- }
5208
- const note = count === 1
5209
- ? '1 setting in this view no longer applies, because the report changed.'
5210
- : `${count} settings in this view no longer apply, because the report changed.`;
5211
- return canChange ? `${note} Save changes to update the view.` : note;
5212
- }
5213
- // The open view as a chat turn names it: its id, and whether the screen
5214
- // differs from it. Both are left out while no view is open.
5215
- function chatViewFields(openView, viewChanged) {
5216
- return openView ? { viewId: openView.id, viewChanged } : {};
5217
- }
5218
- function groupsDiffer(base, current, authored) {
5219
- return Object.entries(current.groups ?? {}).some(([groupId, value]) => {
5220
- const kept = base.groups && Object.prototype.hasOwnProperty.call(base.groups, groupId)
5221
- ? base.groups[Number(groupId)]
5222
- : authored?.groups?.[Number(groupId)];
5223
- return storedValueKey(kept) !== storedValueKey(value);
5224
- });
5055
+ // A range end as a number. A masked input sends its number as text, so
5056
+ // numeric text is read as its number.
5057
+ function finiteNumber(value) {
5058
+ const number = typeof value === 'string' ? numberFromText(value.trim()) : value;
5059
+ return typeof number === 'number' && Number.isFinite(number) ? number : undefined;
5225
5060
  }
5226
5061
  function tilesDiffer(base, current) {
5227
5062
  const ids = new Set([...Object.keys(base), ...Object.keys(current)].map(Number));
5228
5063
  return [...ids].some((componentId) => tileStateKey(base[componentId]) !== tileStateKey(current[componentId]));
5229
5064
  }
5230
- // How many keys of a stored tile state the tile, as it applied, lost: a
5231
- // sort, a search or a drill path it no longer has, and hidden columns it
5232
- // kept fewer of.
5233
- function tileKeysLost(stored, applied) {
5234
- if (!stored) {
5235
- return 0;
5236
- }
5237
- return [
5238
- !!stored.sort?.column && !applied?.sort?.column,
5239
- !!stored.search && !applied?.search,
5240
- (stored.hidden?.length ?? 0) > (applied?.hidden?.length ?? 0),
5241
- (stored.drill?.length ?? 0) > 0 && !applied?.drill?.length,
5242
- ].filter(Boolean).length;
5243
- }
5244
5065
  // One text for a stored group value, the same for every way of writing the
5245
5066
  // same filter: picks in any order, text trimmed, and an empty value read as
5246
5067
  // none.
@@ -5278,281 +5099,23 @@ function keptPicks(list) {
5278
5099
  const kept = (list ?? []).filter((pick) => isSet(pick) && pick !== '');
5279
5100
  return kept.length ? kept : undefined;
5280
5101
  }
5281
- // A range end as a number. A masked input sends its number as text, so
5282
- // numeric text is read as its number, as boundPart reads it.
5283
- function finiteNumber(value) {
5284
- const number = typeof value === 'string' ? numberFromText(value.trim()) : value;
5285
- return typeof number === 'number' && Number.isFinite(number) ? number : undefined;
5286
- }
5287
- // Whether a stored value reads the same as no value at all.
5288
- function holdsNothing(stored) {
5289
- return storedValueKey(stored) === storedValueKey(null);
5290
- }
5291
5102
  // Only the parts that hold something, or null when none does.
5292
5103
  function storedOrNull(value) {
5293
- const stored = compact(value);
5104
+ const stored = Object.fromEntries(Object.entries(value).filter(([, part]) => isSet(part)));
5294
5105
  return Object.keys(stored).length ? stored : null;
5295
5106
  }
5296
- function requireGroupKeys(value, type) {
5297
- const allowed = GROUP_VALUE_KEYS[type] ?? GROUP_VALUE_KEYS.keyword;
5298
- const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
5299
- if (unknown.length) {
5300
- throw new Error(`A ${type} default takes ${allowed.join(', ')} — not ${unknown.join(', ')}.`);
5301
- }
5302
- }
5303
- // A relative range, or fixed days; never both.
5304
- function dateRangeValue(value) {
5305
- const relative = textPart(value, 'relative');
5306
- const start = dayPart(value, 'start');
5307
- const end = dayPart(value, 'end');
5308
- if (relative === '') {
5309
- return compact({ start, end });
5310
- }
5311
- if (start !== null || end !== null) {
5312
- throw new Error('A dateRange default is either a relative preset or a fixed start/end, not both.');
5313
- }
5314
- if (!isRelativeRange(relative)) {
5315
- throw new Error(`relative must be one of: ${Object.keys(RELATIVE_RANGES).join(', ')}.`);
5316
- }
5317
- return { relative };
5318
- }
5319
- // The values to keep or the values to leave out; never both.
5320
- function selectValue(value) {
5321
- const values = pickList(value, 'values');
5322
- const excludeValues = pickList(value, 'excludeValues');
5323
- if (values !== null && excludeValues !== null) {
5324
- throw new Error('A select default either keeps values or leaves out exclude_values, not both.');
5325
- }
5326
- return compact({ values, excludeValues });
5327
- }
5328
- function keywordValue(value) {
5329
- const text = textPart(value, 'value');
5330
- return text !== '' ? { value: text } : {};
5331
- }
5332
- // Either end may be open; 0 is an end like any other.
5333
- function numberRangeValue(value) {
5334
- const min = boundPart(value, 'min');
5335
- const max = boundPart(value, 'max');
5336
- if (min !== null && max !== null && min > max) {
5337
- throw new Error(`min (${min}) is above max (${max}).`);
5338
- }
5339
- return compact({ min, max });
5340
- }
5341
- // The text `key` holds, trimmed, or '' when absent. A number is read as its
5342
- // text; any other kind is refused.
5343
- function textPart(value, key) {
5344
- const text = value[key];
5345
- if (text === undefined || text === null) {
5346
- return '';
5347
- }
5348
- if (typeof text !== 'string' && typeof text !== 'number') {
5349
- throw new Error(`${key} must be text.`);
5350
- }
5351
- return String(text).trim();
5352
- }
5353
- // A calendar-day bound: its first ten characters must be a real day.
5354
- function dayPart(value, key) {
5355
- const text = textPart(value, key);
5356
- if (text === '') {
5357
- return null;
5358
- }
5359
- const day = text.slice(0, 10);
5360
- if (!isCalendarDay(day)) {
5361
- throw new Error(`${key} must be a yyyy-MM-dd day.`);
5362
- }
5363
- return day;
5364
- }
5365
- // The picks of `key` with blanks (null, '') dropped, or null when none is
5366
- // left. A lone pick is a list of one; a pick that is a list or an object is
5367
- // refused.
5368
- function pickList(value, key) {
5369
- const raw = value[key] ?? [];
5370
- const picks = Array.isArray(raw) ? raw : [raw];
5371
- if (picks.some((pick) => isSet(pick) && !isScalar(pick))) {
5372
- throw new Error(`Each pick in ${key} must be a plain value, not a list.`);
5373
- }
5374
- const kept = picks.filter((pick) => isSet(pick) && pick !== '');
5375
- return kept.length ? kept : null;
5376
- }
5377
- // One end of a number range: null when absent or blank, else a finite number
5378
- // (numeric text is read as its number).
5379
- function boundPart(value, key) {
5380
- const raw = typeof value[key] === 'string' ? value[key].trim() : value[key];
5381
- if (!isSet(raw) || raw === '') {
5382
- return null;
5383
- }
5384
- const bound = typeof raw === 'string' ? numberFromText(raw) : raw;
5385
- if (typeof bound !== 'number' || !Number.isFinite(bound)) {
5386
- throw new Error(`${key} must be a finite number.`);
5387
- }
5388
- return bound;
5389
- }
5390
- function requireSize(doc) {
5391
- if (new TextEncoder().encode(JSON.stringify(doc)).length > VIEW_CONFIG_MAX_BYTES) {
5392
- throw new Error('A view config is over 32 KB.');
5393
- }
5394
- }
5395
- function requireVersion(version) {
5396
- if (!isSet(version)) {
5397
- throw new Error('A view config needs its version (v).');
5398
- }
5399
- if (version !== REPORT_VIEW_CONFIG_VERSION) {
5400
- throw new Error(`This viewer reads view config version ${REPORT_VIEW_CONFIG_VERSION} only.`);
5401
- }
5402
- }
5403
- function requireMatch(match) {
5404
- if (match !== 'all' && match !== 'any') {
5405
- throw new Error('match must be all or any.');
5406
- }
5407
- return match;
5408
- }
5409
- // Group id -> value. A value no group type could hold is dropped whole.
5410
- function viewGroups(raw) {
5411
- const entries = Object.entries(requireObject(raw, 'groups must map group ids to values.'));
5412
- if (entries.length > VIEW_CONFIG_MAX_GROUPS) {
5413
- throw new Error(`A view holds at most ${VIEW_CONFIG_MAX_GROUPS} groups.`);
5414
- }
5415
- const groups = {};
5416
- entries.forEach(([key, value]) => {
5417
- const id = requireIdKey(key, 'groups');
5418
- requirePickCount(value);
5419
- const kept = value === null ? null : storedGroupValue(value);
5420
- if (kept !== undefined) {
5421
- groups[id] = kept;
5422
- }
5423
- });
5424
- return groups;
5425
- }
5426
- // A stored group value, read for the one type its keys belong to (an empty
5427
- // value fits every type), or undefined when it fits none.
5428
- function storedGroupValue(value) {
5429
- if (!isPlainObject(value)) {
5430
- return undefined;
5431
- }
5432
- const keys = Object.keys(value);
5433
- const type = Object.keys(GROUP_VALUE_KEYS)
5434
- .find((candidate) => keys.every((key) => GROUP_VALUE_KEYS[candidate].includes(key)));
5435
- return type ? (groupValueFromStored(value, type) ?? undefined) : undefined;
5436
- }
5437
- function requirePickCount(value) {
5438
- const lists = isPlainObject(value) ? [value.values, value.excludeValues] : [];
5439
- if (lists.some((list) => Array.isArray(list) && list.length > GROUP_VALUE_MAX_PICKS)) {
5440
- throw new Error(`A group value holds at most ${GROUP_VALUE_MAX_PICKS} values.`);
5441
- }
5442
- }
5443
- // Component id -> tile state.
5444
- function viewTiles(raw) {
5445
- const entries = Object.entries(requireObject(raw, 'tiles must map component ids to tile states.'));
5446
- if (entries.length > VIEW_CONFIG_MAX_TILES) {
5447
- throw new Error(`A view holds at most ${VIEW_CONFIG_MAX_TILES} tiles.`);
5448
- }
5449
- const tiles = {};
5450
- entries.forEach(([key, value]) => {
5451
- tiles[requireIdKey(key, 'tiles')] = viewTile(value);
5452
- });
5453
- return tiles;
5454
- }
5455
- function viewTile(raw) {
5456
- const tile = requireObject(raw, 'A tile state must be an object.');
5457
- const state = otherKeys(tile, TILE_STATE_KEYS);
5458
- if (isSet(tile.sort)) {
5459
- state.sort = viewSort(tile.sort);
5460
- }
5461
- if (isSet(tile.search)) {
5462
- state.search = requireString(tile.search, 'search must be text.');
5463
- }
5464
- if (isSet(tile.hidden)) {
5465
- state.hidden = viewHidden(tile.hidden);
5466
- }
5467
- if (isSet(tile.drill)) {
5468
- state.drill = viewDrill(tile.drill);
5469
- }
5470
- return state;
5471
- }
5472
- function viewSort(raw) {
5473
- const sort = requireObject(raw, 'sort must be a column and a direction.');
5474
- const column = requireString(sort.column, 'sort must name a column.');
5475
- if (column === '' || (isSet(sort.direction) && sort.direction !== 'asc' && sort.direction !== 'desc')) {
5476
- throw new Error('sort must be a column and a direction.');
5477
- }
5478
- return sort.direction ? { column, direction: sort.direction } : { column };
5479
- }
5480
- function viewHidden(raw) {
5481
- if (!Array.isArray(raw) || raw.some((name) => typeof name !== 'string')) {
5482
- throw new Error('hidden must list column names.');
5483
- }
5484
- return [...raw];
5485
- }
5486
- function viewDrill(raw) {
5487
- if (!Array.isArray(raw)) {
5488
- throw new Error('drill must list steps.');
5489
- }
5490
- if (raw.length > DRILL_MAX_DEPTH) {
5491
- throw new Error(`A drill path holds at most ${DRILL_MAX_DEPTH} steps.`);
5492
- }
5493
- return raw.map((step) => {
5494
- if (!isPlainObject(step) || !isChartGranularity(step.level) || !isDrillValue(step.value)) {
5495
- throw new Error('A drill step needs a time level and a value.');
5496
- }
5497
- return { level: step.level, value: step.value };
5498
- });
5499
- }
5500
5107
  function isDrillValue(value) {
5501
5108
  return value === null || typeof value === 'string' || typeof value === 'number';
5502
5109
  }
5503
- // The keys of `source` that are not in `known`, as they were.
5504
- function otherKeys(source, known) {
5505
- return Object.fromEntries(Object.entries(source).filter(([key]) => !known.includes(key)));
5506
- }
5507
- function optionalId(raw, refusal) {
5508
- if (!isSet(raw)) {
5509
- return null;
5510
- }
5511
- if (!Number.isInteger(raw) || raw < 1) {
5512
- throw new Error(refusal);
5513
- }
5514
- return raw;
5515
- }
5516
- function requireIdKey(key, where) {
5517
- if (!ID_KEY_PATTERN.test(key)) {
5518
- throw new Error(`${where} are keyed by id, not by '${key}'.`);
5519
- }
5520
- return Number(key);
5521
- }
5522
- function requireGranularity(raw, refusal) {
5523
- if (!isChartGranularity(raw)) {
5524
- throw new Error(refusal);
5525
- }
5526
- return raw;
5527
- }
5528
- function requireString(raw, refusal) {
5529
- if (typeof raw !== 'string') {
5530
- throw new Error(refusal);
5531
- }
5532
- return raw;
5533
- }
5534
- function requireObject(raw, refusal) {
5535
- if (!isPlainObject(raw)) {
5536
- throw new Error(refusal);
5537
- }
5538
- return raw;
5539
- }
5540
5110
  function isSet(value) {
5541
5111
  return value !== undefined && value !== null;
5542
5112
  }
5543
- function isPlainObject(value) {
5113
+ function isPlainObject$1(value) {
5544
5114
  return value !== null && typeof value === 'object' && !Array.isArray(value);
5545
5115
  }
5546
- function isScalar(value) {
5547
- return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
5548
- }
5549
- // Only the parts that hold something.
5550
- function compact(value) {
5551
- return Object.fromEntries(Object.entries(value).filter(([, part]) => isSet(part)));
5552
- }
5553
5116
 
5554
5117
  // The words a file uses for what the screen held: each filter's value, the
5555
- // group-by grain, a drilled tile, the match and the open view. The PDF and
5118
+ // group-by grain, a drilled tile and the match. The PDF and
5556
5119
  // PowerPoint covers print these lines, and the zip sends them (filterLines,
5557
5120
  // with filterSummary as their text for an older server), so every export
5558
5121
  // words one filter one way. Pure, so the node harness checks every wording.
@@ -5626,16 +5189,6 @@ function matchLineText(match, form) {
5626
5189
  function drillLineText(component, path, formatContext) {
5627
5190
  return crumbedTitle(component.title, drillCrumbLabels(path, formatContext));
5628
5191
  }
5629
- // The cover's line for the open saved view: 'View: "Q3 North" (shared)',
5630
- // with ', changed since it was opened' while the screen differs from it.
5631
- // Null when no view is open.
5632
- function viewLineText(view, changed) {
5633
- if (!view) {
5634
- return null;
5635
- }
5636
- const line = `View: "${(view.name ?? '').trim()}" (${view.shared ? 'shared' : 'personal'})`;
5637
- return changed ? `${line}, changed since it was opened` : line;
5638
- }
5639
5192
  // The filter lines of an export, in order: one per filter group with a
5640
5193
  // value (the report's group order), the group-by line, then one per drilled
5641
5194
  // chart (page order). A This Page export (input.pageId) lists only the
@@ -5772,10 +5325,10 @@ function drillLines(input) {
5772
5325
  return (report.pages ?? [])
5773
5326
  .filter((page) => !pageId || page.id === pageId)
5774
5327
  .flatMap((page) => page.components ?? [])
5775
- .filter((component) => component.type === 'chart' && !!snapshot.view.tiles?.[component.id]?.drill?.length)
5328
+ .filter((component) => component.type === 'chart' && !!snapshot.state.tiles?.[component.id]?.drill?.length)
5776
5329
  .map((component) => ({
5777
5330
  kind: 'drill',
5778
- text: drillLineText({ title: input.titles?.[component.id] ?? component.title }, snapshot.view.tiles[component.id].drill, snapshot.formatContext),
5331
+ text: drillLineText({ title: input.titles?.[component.id] ?? component.title }, snapshot.state.tiles[component.id].drill, snapshot.formatContext),
5779
5332
  componentId: component.id,
5780
5333
  }));
5781
5334
  }
@@ -5979,8 +5532,8 @@ const HEADER_FOOTER_MAX = 3;
5979
5532
  function isTimeSeriesChart(component) {
5980
5533
  return component.type === 'chart' && component.config?.xAxis?.kind === 'time';
5981
5534
  }
5982
- // The interaction state one tile's data request carries, read off a view
5983
- // document of the screen (the state service's snapshot()): the group-by grain
5535
+ // The interaction state one tile's data request carries, read off a snapshot
5536
+ // of the screen (the state service's snapshot()): the group-by grain
5984
5537
  // for a time-series chart, a list's sort, search and hidden columns, a
5985
5538
  // chart's drill path, and, while the groups match 'any', the groups that
5986
5539
  // combine (matchAnyGroupIds). The tile on screen, its CSV, the PDF and
@@ -6504,16 +6057,17 @@ function emptyTemporaryState() {
6504
6057
  // columns, the page and each chart's drill path. None of it is ever written to
6505
6058
  // the browser URL. It outlives a page switch (the canvas changes the page, not
6506
6059
  // this service) and goes with the report view: a reload starts a new service,
6507
- // which opens on the report's defaults or the viewer's default view.
6508
- // changes() fires on every change of it.
6060
+ // which opens on the report's authored defaults. changes() fires on every
6061
+ // change of it.
6509
6062
  //
6510
- // The durable part is one view document: snapshot() writes it, apply() opens
6511
- // a view, and base and viewChanged say whether the screen still shows that
6512
- // view. The temporary layer (a cross-filter, an open rows panel) is its own
6513
- // section, so no view ever carries it.
6063
+ // The durable part reads as one document: snapshot() writes it, and
6064
+ // differsFromDefaults says whether the screen still shows the report's
6065
+ // authored defaults (resetToDefaults() puts it back on them). The temporary
6066
+ // layer (a cross-filter, an open rows panel) is its own section, so a
6067
+ // snapshot never carries it.
6514
6068
  //
6515
- // Session-only, never in a view: per-filter enable/disable toggles from a
6516
- // component's filter menu (the `enabled` column is the authored default; a
6069
+ // Session-only, never in the snapshot: per-filter enable/disable toggles from
6070
+ // a component's filter menu (the `enabled` column is the authored default; a
6517
6071
  // viewer's toggle shouldn't write to the report).
6518
6072
  class ReportFilterStateService {
6519
6073
  // The open report's effective zone (resolveReportTimezone). Relative presets
@@ -6536,14 +6090,10 @@ class ReportFilterStateService {
6536
6090
  listHidden = signal(new Map(), {
6537
6091
  equal: sameHidden,
6538
6092
  });
6539
- // Moves each time apply() or resetToBase() replaces the whole durable
6540
- // state. Bars and tiles key on it, so an applied view repaints all of them.
6093
+ // Moves each time resetToDefaults() replaces the whole durable state. Bars
6094
+ // and tiles key on it, so a reset repaints all of them; the tiles refetch
6095
+ // in place, so their data stays on screen while they reload.
6541
6096
  stateVersion = computed(() => this._stateVersion());
6542
- // Moves each time apply() opens a view (or the report's defaults). The
6543
- // canvas keys its tiles on it, so an opened view draws each one again.
6544
- // resetToBase() leaves it: a reset refetches the tiles in place, so their
6545
- // data stays on screen while they reload.
6546
- appliedVersion = computed(() => this._appliedVersion());
6547
6097
  // By list component id, how many resets moved that list's sort. FsList
6548
6098
  // reads its sort only when it is created, so a list whose sort a reset
6549
6099
  // moved is drawn again (keyed on its count); every other tile reloads in
@@ -6561,20 +6111,12 @@ class ReportFilterStateService {
6561
6111
  // The report init() last read, or null before one is open. A tile header
6562
6112
  // lists its select options from it (ReportFilterOptionsService).
6563
6113
  report = computed(() => this._report());
6564
- // The state the screen is compared with: the base apply() was given (the
6565
- // open view as it applied), or the report's authored defaults
6566
- // (authoredConfig()) when no view is open. It holds only the groups the
6567
- // view held; every other group follows its authored default.
6568
- base = computed(() => {
6114
+ // Whether the screen differs from the report's authored defaults
6115
+ // (stateDiffers): what the report bar's 'Clear filters' shows for. A switch
6116
+ // of page does not count, and the temporary state never does.
6117
+ differsFromDefaults = computed(() => {
6569
6118
  this._revision();
6570
- return this._baseConfig() ?? this.authoredConfig();
6571
- });
6572
- // Whether the screen differs from base (viewDiffers): what a view picker
6573
- // marks '(edited)'. A switch of page does not count, and the temporary
6574
- // state never does.
6575
- viewChanged = computed(() => {
6576
- this._revision();
6577
- return viewDiffers(this.base(), this.snapshot(), this.authoredConfig());
6119
+ return stateDiffers(this.authoredConfig(), this.snapshot());
6578
6120
  });
6579
6121
  _values = new Map();
6580
6122
  _groups = new Map();
@@ -6585,7 +6127,7 @@ class ReportFilterStateService {
6585
6127
  _cleared = new Set();
6586
6128
  // The report's group-by control: a single report-wide bucket override for all
6587
6129
  // time-series charts. Unset = each chart uses its authored granularity. Never
6588
- // saved to the report (a saved view keeps it), and carried in the data request's
6130
+ // saved to the report, and carried in the data request's
6589
6131
  // `state`, so it propagates through to the PDF/PowerPoint exports too. Named
6590
6132
  // for the `frequency` wire key it travels under, not for the control's label.
6591
6133
  _frequency = null;
@@ -6613,10 +6155,11 @@ class ReportFilterStateService {
6613
6155
  // Fires whenever anything in the viewer state may have changed.
6614
6156
  _stateChanged$ = new Subject();
6615
6157
  // Counts the changes changes() reports. The group values, the grain and
6616
- // the list state that base and viewChanged compare are the plain fields
6158
+ // the list state that differsFromDefaults compares are the plain fields
6617
6159
  // above, which every setter reports through changes() (they are shared
6618
6160
  // with the inline values build, so they stay as they are); this counter is
6619
- // how the two computeds hear of them. The view state below is signals and
6161
+ // how the computed hears of them. The page, match and drills below are
6162
+ // signals and
6620
6163
  // is read directly. The subject is this service's own and is never
6621
6164
  // completed, so the signal listens to it exactly as long as the service
6622
6165
  // lives.
@@ -6625,13 +6168,11 @@ class ReportFilterStateService {
6625
6168
  manualCleanup: true,
6626
6169
  });
6627
6170
  _stateVersion = signal(0);
6628
- _appliedVersion = signal(0);
6629
6171
  _listSortResets = signal(new Map());
6630
- // The report init() last read: the tiles and pages a view's state is
6631
- // checked against.
6172
+ // The report init() last read: the tiles and pages the state is checked
6173
+ // against.
6632
6174
  _report = signal(null);
6633
- // The open page, set by the host as the viewer moves between pages. A view
6634
- // stores it, but moving to another page never edits a view.
6175
+ // The open page, set by the host as the viewer moves between pages.
6635
6176
  _pageId = signal(null);
6636
6177
  // How the groups combine: 'all' (the default) or 'any'.
6637
6178
  _match = signal('all');
@@ -6640,11 +6181,8 @@ class ReportFilterStateService {
6640
6181
  _drills = signal(new Map());
6641
6182
  // Emits the id of the chart whose drill path changed.
6642
6183
  _drillChanged$ = new Subject();
6643
- // The base as apply() applied it, or null when the base is the authored
6644
- // defaults (see base).
6645
- _baseConfig = signal(null);
6646
6184
  // The temporary layer: kept apart from everything above, so snapshot()
6647
- // and viewChanged never read it.
6185
+ // and differsFromDefaults never read it.
6648
6186
  _temporary = signal(emptyTemporaryState());
6649
6187
  // Seed groups + their defaults when a report loads. Existing session values
6650
6188
  // for still-existing groups survive a structure re-fetch (a chat edit
@@ -6674,7 +6212,7 @@ class ReportFilterStateService {
6674
6212
  }
6675
6213
  this._lists = new Map(this._listComponents(report).map((component) => [component.id, component]));
6676
6214
  this._prune(report);
6677
- this._pruneViewState(report, previousGroups);
6215
+ this._prunePageState(report);
6678
6216
  this._changed$.next(null);
6679
6217
  this._stateChanged$.next();
6680
6218
  }
@@ -6701,7 +6239,7 @@ class ReportFilterStateService {
6701
6239
  this._stateChanged$.next();
6702
6240
  }
6703
6241
  // A relative preset name as today's range in the report's zone, or null for
6704
- // a name it does not know. A view's rolling range is opened through this.
6242
+ // a name it does not know.
6705
6243
  resolveRelative(name) {
6706
6244
  return this._relativeRange(name);
6707
6245
  }
@@ -6749,8 +6287,8 @@ class ReportFilterStateService {
6749
6287
  // Whether a list shows the search box over its text columns: it does when the
6750
6288
  // server answers a list search (see _serverSearches), unless one of its live
6751
6289
  // filters belongs to a keyword group its header shows. A bar shows one search
6752
- // box (its last keyword item), and that one is the author's. The header, an
6753
- // opened view, the pruning, the data request state and the exports all read
6290
+ // box (its last keyword item), and that one is the author's. The header, the
6291
+ // pruning, the data request state and the exports all read
6754
6292
  // this one rule, so no search narrows a list whose screen has no box showing
6755
6293
  // it, and no export titles a search the server did not apply.
6756
6294
  listSearchable(component) {
@@ -6810,7 +6348,7 @@ class ReportFilterStateService {
6810
6348
  }
6811
6349
  // Fires whenever the viewer state may have changed: a group value, the
6812
6350
  // grain, the match, the page, a list sort, search or hidden columns, a
6813
- // drill path, an applied view or a (re)load of the report.
6351
+ // drill path, a reset or a (re)load of the report.
6814
6352
  changes() {
6815
6353
  return this._stateChanged$.asObservable();
6816
6354
  }
@@ -6949,117 +6487,86 @@ class ReportFilterStateService {
6949
6487
  this._stateChanged$.next();
6950
6488
  return true;
6951
6489
  }
6952
- // The durable state on screen as one view document: the page, every group
6953
- // the report has (null when it holds nothing, a cleared default included),
6954
- // the match, the grain, and each tile's sort, search, hidden columns and
6955
- // drill path where they differ from the authored tile. A date range that
6956
- // still carries its relative preset is written as that preset, whatever
6957
- // today is (toStoredGroupValue). The temporary layer is never part of it.
6490
+ // The durable state on screen as one document: the page, every group the
6491
+ // report has (null when it holds nothing, a cleared default included), the
6492
+ // match, the grain, and each tile's sort, search, hidden columns and drill
6493
+ // path where they differ from the authored tile. A date range that still
6494
+ // carries its relative preset is written as that preset, whatever today is
6495
+ // (toStoredGroupValue). The temporary layer is never part of it.
6958
6496
  snapshot() {
6959
- const config = { v: REPORT_VIEW_CONFIG_VERSION };
6497
+ const snapshot = {};
6960
6498
  if (this._pageId()) {
6961
- config.pageId = this._pageId();
6499
+ snapshot.pageId = this._pageId();
6962
6500
  }
6963
6501
  if (this._frequency) {
6964
- config.groupBy = this._frequency;
6502
+ snapshot.groupBy = this._frequency;
6965
6503
  }
6966
6504
  if (this._match() === 'any') {
6967
- config.match = 'any';
6505
+ snapshot.match = 'any';
6968
6506
  }
6969
- config.groups = this._snapshotGroups();
6507
+ snapshot.groups = this._snapshotGroups();
6970
6508
  const tiles = this._snapshotTiles();
6971
6509
  if (Object.keys(tiles).length) {
6972
- config.tiles = tiles;
6510
+ snapshot.tiles = tiles;
6973
6511
  }
6974
- return config;
6512
+ return snapshot;
6975
6513
  }
6976
- // The report's authored defaults as a view document: every group at its
6977
- // authored default (null when it has none), no match, grain or tile state.
6514
+ // The report's authored defaults as a snapshot: every group at its authored
6515
+ // default (null when it has none), no match, grain or tile state.
6978
6516
  authoredConfig() {
6979
6517
  const groups = {};
6980
6518
  this._groups.forEach((group) => {
6981
6519
  groups[group.id] = toStoredGroupValue(group.type, this._defaultValue(group));
6982
6520
  });
6983
- return { v: REPORT_VIEW_CONFIG_VERSION, groups };
6984
- }
6985
- // Replaces the durable state with a view's (`config`; null = the report's
6986
- // authored defaults), which becomes the base: what the screen is compared
6987
- // with for '(edited)' and what resetToBase() returns to. A group the view
6988
- // does not hold opens at its authored default; one the report does not
6989
- // have, or whose value does not fit the group's type, is skipped. The
6990
- // view's page opens, unless `pageId` names a page of the report (a ?page=
6991
- // the report opened with beats its default view's page). The temporary
6992
- // layer is kept. Every change is announced once, at the end.
6993
- apply(config, pageId = null) {
6994
- const frequency = this._frequency;
6995
- this._setBase(config);
6996
- this._applyPage(config?.pageId);
6997
- this._applyPage(pageId);
6998
- this._appliedVersion.update((version) => version + 1);
6999
- this._notifyApplied(frequency);
7000
- }
7001
- // Puts the screen back on its base (the open view, else the authored
7002
- // defaults) on the page it is on, and clears the temporary layer. The
7003
- // tiles stay and reload in place (appliedVersion does not move), except a
7004
- // list whose sort this moved (listSortResets).
7005
- resetToBase() {
6521
+ return { groups };
6522
+ }
6523
+ // Puts the screen back on the report's authored defaults on the page it is
6524
+ // on: every group at its default, match 'all', no grain, and every tile at
6525
+ // its authored sort, search, columns and top. Clears the temporary layer.
6526
+ // The tiles stay and reload in place, except a list whose sort this moved
6527
+ // (listSortResets). Every change is announced once, at the end.
6528
+ resetToDefaults() {
7006
6529
  const frequency = this._frequency;
7007
6530
  const sorts = new Map(this._listSorts);
7008
6531
  this._temporary.set(emptyTemporaryState());
7009
- this._applyLayer(this._baseConfig());
6532
+ this._groups.forEach((group) => this._seedAuthored(group));
6533
+ this._match.set('all');
6534
+ this._frequency = null;
6535
+ this._clearTiles();
7010
6536
  this._countSortResets(sorts);
7011
- this._notifyApplied(frequency);
7012
- }
7013
- // The screen as it is becomes the base, with nothing applied: what a view
7014
- // just saved from the screen holds. The page on screen is the base's.
7015
- setBaseToScreen() {
7016
- this._baseConfig.set(this.snapshot());
7017
- this._stateChanged$.next();
7018
- }
7019
- // The report's authored defaults become the base again (the open view was
7020
- // deleted); the screen stays as it is.
7021
- clearBase() {
7022
- this._baseConfig.set(null);
7023
- this._stateChanged$.next();
7024
- }
7025
- // Puts only `groupIds` back on the base: the open view's value for a group
7026
- // it holds, else the group's authored default. A tile header's 'Clear
7027
- // filters' owns only its own groups; the report bar's calls resetToBase().
7028
- // Each group is announced as a viewer's change to it is, so the tiles in
7029
- // it refetch and a bar showing it with another value is built again.
7030
- resetGroupsToBase(groupIds) {
7031
- const held = this._baseConfig()?.groups ?? {};
6537
+ this._notifyReset(frequency);
6538
+ }
6539
+ // Puts only `groupIds` back on their authored defaults. A tile header's
6540
+ // 'Clear filters' owns only its own groups; the report bar's calls
6541
+ // resetToDefaults(). Each group is announced as a viewer's change to it is,
6542
+ // so the tiles in it refetch and a bar showing it with another value is
6543
+ // built again.
6544
+ resetGroupsToDefaults(groupIds) {
7032
6545
  groupIds.forEach((groupId) => {
7033
6546
  const group = this._groups.get(groupId);
7034
6547
  if (!group) {
7035
6548
  return;
7036
6549
  }
7037
- if (!Object.prototype.hasOwnProperty.call(held, groupId) || !this._applyGroup(group, held[groupId])) {
7038
- this._seedAuthored(group);
7039
- }
6550
+ this._seedAuthored(group);
7040
6551
  this._changed$.next(groupId);
7041
6552
  });
7042
6553
  this._stateChanged$.next();
7043
6554
  }
7044
- // Whether one of `groupIds` shows a value other than its base: the open
7045
- // view's value for a group it holds, else the group's authored default,
7046
- // the value resetGroupsToBase() puts back and viewChanged compares. A tile
7047
- // header's Clear chip shows exactly while one of its own groups does. It
7048
- // reads the revision viewChanged reads, so a caller on each check of an
7049
- // OnPush view stays current.
7050
- groupsDifferFromBase(groupIds) {
6555
+ // Whether one of `groupIds` shows a value other than its authored default,
6556
+ // the value resetGroupsToDefaults() puts back. A tile header's Clear chip
6557
+ // shows exactly while one of its own groups does. It reads the revision
6558
+ // differsFromDefaults reads, so a caller on each check of an OnPush view
6559
+ // stays current.
6560
+ groupsDifferFromDefaults(groupIds) {
7051
6561
  this._revision();
7052
- const held = this._baseConfig()?.groups ?? {};
7053
6562
  return groupIds.some((groupId) => {
7054
6563
  const group = this._groups.get(groupId);
7055
6564
  if (!group) {
7056
6565
  return false;
7057
6566
  }
7058
- const base = Object.prototype.hasOwnProperty.call(held, groupId)
7059
- ? held[groupId]
7060
- : toStoredGroupValue(group.type, this._defaultValue(group));
6567
+ const authored = toStoredGroupValue(group.type, this._defaultValue(group));
7061
6568
  const screen = this._cleared.has(groupId) ? null : toStoredGroupValue(group.type, this._values.get(groupId));
7062
- return !sameStoredGroupValue(base, screen);
6569
+ return !sameStoredGroupValue(authored, screen);
7063
6570
  });
7064
6571
  }
7065
6572
  // The interaction state a tile's data request carries now: tileDataState
@@ -7326,77 +6833,6 @@ class ReportFilterStateService {
7326
6833
  }
7327
6834
  return tiles;
7328
6835
  }
7329
- // The view apply() was given, applied as the screen's state and kept as
7330
- // the base. What is kept is the snapshot of it, with only the groups it
7331
- // held: a group it did not hold, or held in a shape that does not fit,
7332
- // follows its authored default (viewDiffers compares it with
7333
- // authoredConfig()).
7334
- _setBase(base) {
7335
- const held = this._applyLayer(base);
7336
- if (!base) {
7337
- this._baseConfig.set(null);
7338
- return;
7339
- }
7340
- // The page is the base's own, not the one on screen.
7341
- const applied = this.snapshot();
7342
- delete applied.pageId;
7343
- applied.groups = Object.fromEntries(Object.entries(applied.groups ?? {})
7344
- .filter(([groupId]) => held.has(Number(groupId))));
7345
- if (base.pageId && (this._report()?.pages ?? []).some((page) => page.id === base.pageId)) {
7346
- applied.pageId = base.pageId;
7347
- }
7348
- this._baseConfig.set(applied);
7349
- }
7350
- // Applies a view's state in place of the whole durable state, and gives the
7351
- // ids of the groups it set: what `config` does not hold goes back to the
7352
- // authored report. Nothing is announced here: the caller announces once
7353
- // (_notifyApplied).
7354
- _applyLayer(config) {
7355
- const held = this._applyGroups(config?.groups ?? {});
7356
- this._match.set(config?.match === 'any' ? 'any' : 'all');
7357
- const groupBy = config?.groupBy;
7358
- this._frequency = isChartGranularity(groupBy) ? groupBy : null;
7359
- this._clearTiles();
7360
- Object.entries(config?.tiles ?? {})
7361
- .forEach(([componentId, tile]) => this._applyTile(Number(componentId), tile ?? {}));
7362
- return held;
7363
- }
7364
- // Each group's value from a view; a group it does not set goes back to its
7365
- // authored default.
7366
- _applyGroups(groups) {
7367
- const held = new Set();
7368
- this._groups.forEach((group) => {
7369
- if (Object.prototype.hasOwnProperty.call(groups, group.id) && this._applyGroup(group, groups[group.id])) {
7370
- held.add(group.id);
7371
- }
7372
- else {
7373
- this._seedAuthored(group);
7374
- }
7375
- });
7376
- return held;
7377
- }
7378
- // One group's stored value, or false when the group's type cannot hold it
7379
- // (it is then dropped whole, and the caller seeds the group's authored
7380
- // default). null, or a value that holds nothing, clears the group.
7381
- _applyGroup(group, stored) {
7382
- const kept = stored === null ? {} : groupValueFromStored(stored, group.type);
7383
- if (kept === null) {
7384
- return false;
7385
- }
7386
- const value = toLiveGroupValue(group.type, kept, (name) => this.resolveRelative(name));
7387
- if (value) {
7388
- this._values.set(group.id, value);
7389
- this._cleared.delete(group.id);
7390
- }
7391
- else {
7392
- this._values.delete(group.id);
7393
- this._cleared.delete(group.id);
7394
- if (group.config?.default) {
7395
- this._cleared.add(group.id);
7396
- }
7397
- }
7398
- return true;
7399
- }
7400
6836
  // A group back at its authored default.
7401
6837
  _seedAuthored(group) {
7402
6838
  const value = this._defaultValue(group);
@@ -7415,54 +6851,6 @@ class ReportFilterStateService {
7415
6851
  this.listHidden.set(new Map());
7416
6852
  this._drills.set(new Map());
7417
6853
  }
7418
- // One tile's stored state, each key checked: a sort on a column the list
7419
- // can sort by, a search on a list that shows the box, hidden columns the
7420
- // list can hide, a drill path on a chart.
7421
- _applyTile(componentId, tile) {
7422
- const list = this._lists.get(componentId);
7423
- if (list) {
7424
- this._applyListTile(list, tile);
7425
- }
7426
- if (Array.isArray(tile.drill) && this._isChart(componentId)) {
7427
- this._setDrillPath(componentId, cleanDrillPath(tile.drill));
7428
- }
7429
- }
7430
- // A list's sort, search and hidden columns from a tile state, kept by the
7431
- // rules setListSort, setListSearch and setHiddenColumns apply, but set
7432
- // straight on the maps: those setters each announce a change, and an apply
7433
- // announces once, at its end. A sort that is the list's authored first
7434
- // term is its default, so it clears the entry.
7435
- _applyListTile(list, tile) {
7436
- const sort = tile.sort;
7437
- if (sort?.column && this._sortableColumns(list).includes(sort.column)) {
7438
- if (sameSort(sort, listSortTerms(list.config)[0] ?? null)) {
7439
- this._listSorts.delete(list.id);
7440
- }
7441
- else {
7442
- this._listSorts.set(list.id, { ...sort });
7443
- }
7444
- }
7445
- if (typeof tile.search === 'string') {
7446
- const search = listSearchText(tile.search);
7447
- if (search && this.listSearchable(list)) {
7448
- this._listSearches.set(list.id, search);
7449
- }
7450
- else {
7451
- this._listSearches.delete(list.id);
7452
- }
7453
- }
7454
- if (Array.isArray(tile.hidden)) {
7455
- const hidden = listHiddenColumns(list.config, tile.hidden);
7456
- const next = new Map(this.listHidden());
7457
- if (hidden.length) {
7458
- next.set(list.id, hidden);
7459
- }
7460
- else {
7461
- next.delete(list.id);
7462
- }
7463
- this.listHidden.set(next);
7464
- }
7465
- }
7466
6854
  // One chart's drill path; an empty path is the chart's top.
7467
6855
  _setDrillPath(componentId, path) {
7468
6856
  const next = new Map(this._drills());
@@ -7474,15 +6862,9 @@ class ReportFilterStateService {
7474
6862
  }
7475
6863
  this._drills.set(next);
7476
6864
  }
7477
- // The page a layer names, when the report has it.
7478
- _applyPage(pageId) {
7479
- if (pageId && (this._report()?.pages ?? []).some((page) => page.id === pageId)) {
7480
- this._pageId.set(pageId);
7481
- }
7482
- }
7483
- // After apply() or resetToBase(): every bar and tile repaints, and the
6865
+ // After resetToDefaults(): every bar and tile repaints, and the
7484
6866
  // time-series charts refetch when the grain moved.
7485
- _notifyApplied(frequency) {
6867
+ _notifyReset(frequency) {
7486
6868
  this._stateVersion.update((version) => version + 1);
7487
6869
  this._changed$.next(null);
7488
6870
  if (frequency !== this._frequency) {
@@ -7501,13 +6883,12 @@ class ReportFilterStateService {
7501
6883
  return this._tiles()
7502
6884
  .some((component) => component.id === componentId && component.type === 'chart');
7503
6885
  }
7504
- // Keeps the view state to what the report (re)loaded by init() still has:
7505
- // drill paths on charts it still has, the page while it has it, and the
7506
- // base to what the screen can still show (_pruneBase). On another report it
7507
- // starts again: no open view, match 'all', no drill and nothing temporary.
7508
- _pruneViewState(report, previousGroups) {
6886
+ // Keeps the page and drill state to what the report (re)loaded by init()
6887
+ // still has: drill paths on charts it still has, the page while it has it.
6888
+ // On another report it starts again: match 'all', no drill and nothing
6889
+ // temporary.
6890
+ _prunePageState(report) {
7509
6891
  if (this._report()?.id !== report.id) {
7510
- this._baseConfig.set(null);
7511
6892
  this._match.set('all');
7512
6893
  this._pageId.set(null);
7513
6894
  this._drills.set(new Map());
@@ -7519,58 +6900,6 @@ class ReportFilterStateService {
7519
6900
  if (this._pageId() && !(report.pages ?? []).some((page) => page.id === this._pageId())) {
7520
6901
  this._pageId.set(null);
7521
6902
  }
7522
- this._pruneBase(previousGroups);
7523
- }
7524
- // Keeps the base to what the screen can still show, by the rules the live
7525
- // state is pruned by (_prune, _seed and the drill pruning above), so an
7526
- // edit of the report alone never reads as '(edited)'. A group whose
7527
- // authored default changed leaves the base and follows the new default,
7528
- // as the live value does (_seed re-seeds it); so does a group the report
7529
- // no longer has. Each tile keeps only the state its component can still
7530
- // take (_baseTile).
7531
- _pruneBase(previousGroups) {
7532
- const base = this._baseConfig();
7533
- if (!base) {
7534
- return;
7535
- }
7536
- const groups = Object.fromEntries(Object.entries(base.groups ?? {})
7537
- .filter(([groupId]) => {
7538
- const group = this._groups.get(Number(groupId));
7539
- return !!group
7540
- && sameDefault(previousGroups.get(group.id)?.config?.default ?? null, group.config?.default ?? null);
7541
- }));
7542
- const tiles = Object.fromEntries(Object.entries(base.tiles ?? {})
7543
- .map(([componentId, tile]) => [componentId, this._baseTile(Number(componentId), tile)])
7544
- .filter(([, tile]) => Object.keys(tile).length));
7545
- const pruned = { ...base, groups };
7546
- delete pruned.tiles;
7547
- if (Object.keys(tiles).length) {
7548
- pruned.tiles = tiles;
7549
- }
7550
- this._baseConfig.set(pruned);
7551
- }
7552
- // A base tile's state, kept to what its component can still take: a sort
7553
- // on a column the list can still sort by that is not now its authored
7554
- // first term, a search while the list shows the box, the hidden columns
7555
- // the list still has, a drill path while the component is a chart.
7556
- _baseTile(componentId, tile) {
7557
- const kept = {};
7558
- const list = this._lists.get(componentId);
7559
- const hidden = list && tile.hidden ? listHiddenColumns(list.config, tile.hidden) : [];
7560
- if (list && tile.sort && this._sortableColumns(list).includes(tile.sort.column)
7561
- && !sameSort(tile.sort, listSortTerms(list.config)[0] ?? null)) {
7562
- kept.sort = tile.sort;
7563
- }
7564
- if (list && tile.search && this.listSearchable(list)) {
7565
- kept.search = tile.search;
7566
- }
7567
- if (hidden.length) {
7568
- kept.hidden = hidden;
7569
- }
7570
- if (tile.drill?.length && this._isChart(componentId)) {
7571
- kept.drill = tile.drill;
7572
- }
7573
- return kept;
7574
6903
  }
7575
6904
  // A select's values to leave out (only when it keeps none) and a number
7576
6905
  // range's ends, an open end left out and 0 kept, written onto `fields`.
@@ -8337,8 +7666,8 @@ function finite(value) {
8337
7666
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
8338
7667
  }
8339
7668
 
8340
- // A sweep of changes (a few filters set in a row, every value of an applied
8341
- // view) settles for this long before a tile refetches, so the sweep costs one
7669
+ // A sweep of changes (a few filters set in a row, every value of a picked
7670
+ // saved filter) settles for this long before a tile refetches, so the sweep costs one
8342
7671
  // request, not one per change.
8343
7672
  const TILE_REFRESH_DEBOUNCE_MS = 300;
8344
7673
  // A tile's data as one stream: the first load at once, then one load per
@@ -8349,9 +7678,8 @@ const TILE_REFRESH_DEBOUNCE_MS = 300;
8349
7678
  // So an answer to an older request never lands, not even while the next
8350
7679
  // load still waits, and the bar stays on until the settled load answers.
8351
7680
  // - Each load notes the state version it was sent under (stateVersion), and
8352
- // its answer is dropped when the version has moved since. apply() and
8353
- // resetToBase() move the version and announce a full change together, so
8354
- // the load for the new state is already on its way.
7681
+ // its answer is dropped when the version has moved since. resetToDefaults()
7682
+ // moves the version and announces a full change together, so the load for the new state is already on its way.
8355
7683
  // - A failed load ends only itself: its message comes through as the answer,
8356
7684
  // and the next trigger loads again.
8357
7685
  function tileLoads(refresh$, load, version) {
@@ -8941,8 +8269,8 @@ class ComponentListComponent {
8941
8269
  // How many leading columns stay in view while the list scrolls sideways.
8942
8270
  pinned = computed(() => listPinnedColumns(this.component().config));
8943
8271
  columnClasses = columnClasses;
8944
- // The config columns the viewer hid on this list (the Columns dialog, or a
8945
- // saved view). fs-list hides and shows them in place through [show], so the rows
8272
+ // The config columns the viewer hid on this list (the Columns dialog). fs-list
8273
+ // hides and shows them in place through [show], so the rows
8946
8274
  // already fetched stand.
8947
8275
  hidden = computed(() => new Set(this._filterState.hiddenColumns(this.component().id)));
8948
8276
  totalLabel = LIST_TOTAL_LABEL;
@@ -10383,7 +9711,7 @@ class ReportComponentComponent {
10383
9711
  // The breadcrumb after the title while the chart is drilled ('2026', 'Q3'),
10384
9712
  // one label per step of its path, in the report's locale. Empty at the top
10385
9713
  // and while the viewer is not offered drill: a path an older server never
10386
- // applies is kept for a saved view, but not shown.
9714
+ // applies is kept in the state, but not shown.
10387
9715
  drillCrumbs = computed(() => (this._offeredFeatures().includes('drill')
10388
9716
  ? drillCrumbLabels(this._filterState.drill(this.component?.id), this._filterState.formatContext())
10389
9717
  : []));
@@ -10433,7 +9761,7 @@ class ReportComponentComponent {
10433
9761
  if (this.component.type !== 'list' && !this.isText && isDrawn(this.component)) {
10434
9762
  // One load stream (tileLoads): the first load at once, then one per
10435
9763
  // settled sweep of changes, each cancelling the one in flight, and an
10436
- // answer sent before a view was applied is dropped.
9764
+ // answer sent before a reset is dropped.
10437
9765
  tileLoads(this._refresh$, () => this._load(), () => this._filterState.stateVersion())
10438
9766
  .pipe(tap$1((result) => this._loaded(result)), takeUntilDestroyed(this._destroyRef))
10439
9767
  .subscribe();
@@ -10592,7 +9920,7 @@ class ReportComponentComponent {
10592
9920
  return this.component.type === 'list' && listHideableColumns(this.component.config).length > 1;
10593
9921
  }
10594
9922
  // Which of a plain list's columns show. The choice is the viewer's (kept in
10595
- // the report's state and a saved view, never saved to the report), and the
9923
+ // the report's state, never saved to the report), and the
10596
9924
  // list hides the columns in place, with no refetch.
10597
9925
  chooseColumns() {
10598
9926
  const columns = (this.component.config?.columns ?? [])
@@ -10816,25 +10144,20 @@ class ReportComponentComponent {
10816
10144
  this._filterState.setListSearch(this.component.id, String(query?.[LIST_SEARCH_ITEM] ?? ''));
10817
10145
  }
10818
10146
  },
10819
- // 'Clear filters' puts this header's own groups back on the open view,
10820
- // else on their authored defaults. fs-filter reports its own cleared
10821
- // items (change) just before this, so the reset wins. A server without
10822
- // saved views keeps the clear it always had: the header's items only.
10823
- clear: features.includes('views')
10824
- ? () => this._filterState.resetGroupsToBase(groups.map((group) => group.id))
10825
- : undefined,
10147
+ // 'Clear filters' puts this header's own groups back on their authored
10148
+ // defaults. fs-filter reports its own cleared items (change) just
10149
+ // before this, so the reset wins.
10150
+ clear: () => this._filterState.resetGroupsToDefaults(groups.map((group) => group.id)),
10826
10151
  // The chip shows exactly while one of those groups differs from what
10827
- // that clear puts back (groupsDifferFromBase). fs-filter's own rule
10828
- // compares the items with the values this header was built with, so it
10829
- // would miss a value the open view holds and offer a clear that changes
10830
- // nothing. A server without saved views keeps fs-filter's own rule.
10831
- clearVisible: features.includes('views')
10832
- ? () => this._filterState.groupsDifferFromBase(groups.map((group) => group.id))
10833
- : undefined,
10152
+ // that clear puts back (groupsDifferFromDefaults). fs-filter's own rule
10153
+ // compares the items with the values this header was built with, which
10154
+ // may be the viewer's own picks, and would offer a clear that changes
10155
+ // nothing.
10156
+ clearVisible: () => this._filterState.groupsDifferFromDefaults(groups.map((group) => group.id)),
10834
10157
  });
10835
10158
  }
10836
- // apply() and resetToBase() move the state version and announce a full
10837
- // change (null). The header is built again for the new version, and
10159
+ // resetToDefaults() moves the state version and announces a full change
10160
+ // (null). The header is built again for the new version, and
10838
10161
  // filterKey, which carries it, recreates the block: fs-filter reads its
10839
10162
  // config once. Any other full change (a reload, the match) leaves it be.
10840
10163
  // A change to one group the header shows with another value (another bar
@@ -10972,7 +10295,7 @@ class ReportComponentComponent {
10972
10295
  }
10973
10296
  // A chart takes the drill path its answer applied (adoptedDrillPath) while
10974
10297
  // the viewer is offered drill; an older server applies none, and the path
10975
- // stays for a saved view. When the path it sent was cut, the viewer is told
10298
+ // stays in the state. When the path it sent was cut, the viewer is told
10976
10299
  // once (drillResetNoted): the shorter path is what the chart reads from
10977
10300
  // now on, so the server cuts nothing again.
10978
10301
  _adoptDrill(data) {
@@ -11116,8 +10439,8 @@ class ReportCanvasComponent {
11116
10439
  // The report's structure changed here (a page was added) — the host re-fetches
11117
10440
  // it (it owns the assembled report + filter state).
11118
10441
  reportChanged = new EventEmitter();
11119
- // The page on screen changed — the host keeps it in the report's state (a
11120
- // saved view stores it). Carries the page id, never the index.
10442
+ // The page on screen changed — the host keeps it in the report's state.
10443
+ // Carries the page id, never the index.
11121
10444
  pageChanged = new EventEmitter();
11122
10445
  zoomPan;
11123
10446
  // The rendered components of the active page — only the canvas can reach the
@@ -11139,10 +10462,6 @@ class ReportCanvasComponent {
11139
10462
  masterMode = signal(false);
11140
10463
  groups = new Map();
11141
10464
  selectedComponentId = signal(null);
11142
- // Moves when a view is opened. Every tile is tracked by it, so each is
11143
- // drawn again from the view's state. A reset to the open view leaves it:
11144
- // the tiles reload in place and keep their data on screen meanwhile.
11145
- appliedVersion = inject(ReportFilterStateService).appliedVersion;
11146
10465
  // The components each layer draws. A value only reads inside text, so one
11147
10466
  // that leaked into a layer's components is still never drawn.
11148
10467
  activePageComponents = computed(() => (this.pages()[this.activePageIndex()]?.components ?? []).filter(isDrawn));
@@ -11565,7 +10884,7 @@ class ReportCanvasComponent {
11565
10884
  this._watchOptionNames();
11566
10885
  }
11567
10886
  // The filter tokens follow every change of a filter's value, however it
11568
- // comes: the bar, a saved view opened, Clear, Reset, a reload. changes()
10887
+ // comes: the bar, a saved filter picked, Clear, a reload. changes()
11569
10888
  // also announces what no token reads (a list's sort or search, its hidden
11570
10889
  // columns, a drill, the page), which moves nothing here.
11571
10890
  _watchFilters() {
@@ -11618,8 +10937,8 @@ class ReportCanvasComponent {
11618
10937
  // Triggers landing together (a reload that also changes the page) make one
11619
10938
  // request, and a newer request drops the one still in flight. Any other
11620
10939
  // change of the screen's state (changes) asks again only when it moved
11621
- // the request: a view applied or the bar reset (stateVersion moves on
11622
- // each), the match changed, or, while the groups match 'any', a group the
10940
+ // the request: the bar reset (stateVersion moves on
10941
+ // it), the match changed, or, while the groups match 'any', a group the
11623
10942
  // values have no member of took or lost a value.
11624
10943
  _watchValues(layer, onScreen$) {
11625
10944
  merge(layer.reload$, onScreen$, this._filterState.changesForAny(() => layer.source()?.values ?? []).pipe(debounceTime(300)), this._filterState.changes().pipe(debounceTime(300), filter$1(() => this._requestMoved(layer))))
@@ -11894,7 +11213,7 @@ class ReportCanvasComponent {
11894
11213
  this._guides?.nativeElement?.replaceChildren();
11895
11214
  }
11896
11215
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11897
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportCanvasComponent, isStandalone: true, selector: "app-report-canvas", inputs: { report: "report", editMode: "editMode", pageId: "pageId" }, outputs: { componentSettings: "componentSettings", reportChanged: "reportChanged", pageChanged: "pageChanged" }, viewQueries: [{ propertyName: "zoomPan", first: true, predicate: FsZoomPanComponent, descendants: true }, { propertyName: "_viewport", first: true, predicate: ["viewport"], descendants: true, static: true }, { propertyName: "_guides", first: true, predicate: ["guides"], descendants: true }, { propertyName: "_renderedComponents", predicate: ReportComponentComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id + ':' + appliedVersion()) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [style.height.px]=\"bands.headerHeight * PX\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [style.height.px]=\"bands.footerHeight * PX\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id + ':' + appliedVersion()) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\n <div class=\"page-empty\">\n Ask the assistant to add a chart, list or KPI to this page.\n </div>\n }\n </div>\n }\n </fs-zoom-pan>\n </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\n }\n <button\n type=\"button\"\n class=\"page-tab add-page\"\n matTooltip=\"Add page\"\n (click)=\"addPage()\">\n <mat-icon>add</mat-icon>\n Page\n </button>\n </div>\n\n <div class=\"zoom-controls\">\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom out (Ctrl/Cmd + scroll)\"\n (click)=\"zoomOut()\">\n <mat-icon>remove</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-label\"\n matTooltip=\"Zoom to 100%\"\n (click)=\"zoomActual()\">\n {{ (zoom() * 100).toFixed(0) }}%\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom in (Ctrl/Cmd + scroll)\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-width:0;min-height:0;flex:1;margin-top:10px}.canvas{display:flex;flex-direction:column;gap:8px;flex:1;min-height:0}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px}.canvas-toolbar .page-tabs{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;cursor:pointer}.canvas-toolbar .page-tabs .page-tab.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.canvas-toolbar .page-tabs .page-tab .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 8px;border-style:dashed;color:#52606d}.canvas-toolbar .page-tabs .page-tab.add-page mat-icon{font-size:16px;width:16px;height:16px}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.canvas-toolbar .zoom-controls{display:flex;align-items:center;gap:2px;margin-left:auto;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .zoom-controls .zoom-button{display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-button:hover{background:#f0f4f8}.canvas-toolbar .zoom-controls .zoom-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .zoom-controls .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-label:hover{color:#1f2933}.page-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.page-viewport{position:relative;flex:1;min-height:420px;border:1px solid #e4e7eb;border-radius:8px;background:#eef1f5;overflow:hidden}.page-viewport fs-zoom-pan{width:100%;height:100%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.guides{position:absolute;inset:0;pointer-events:none;z-index:30}.guides ::ng-deep .guide{position:absolute;background:#2196f3}.guides ::ng-deep .guide.guide-v{top:0;bottom:0;width:calc(1.25px / var(--canvas-zoom, 1))}.guides ::ng-deep .guide.guide-h{left:0;right:0;height:calc(1.25px / var(--canvas-zoom, 1))}.page-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;font-size:14px;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$3.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$3.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "ngmodule", type: FsZoomPanModule }, { kind: "component", type: i3$1.FsZoomPanComponent, selector: "fs-zoom-pan", inputs: ["zoomMax", "zoomMin", "zoomScale", "zoomDefault", "zoomFactor", "zoomFit", "fitMode", "fitPosition", "fitPadding", "fitMaxScale", "wheel", "top", "left"], outputs: ["moved", "zoomed"] }, { kind: "component", type: ReportComponentComponent, selector: "app-report-component", inputs: ["reportId", "component", "groups", "editMode", "zoom", "snapper", "selected", "tokens", "valueKeysElsewhere"], outputs: ["selectComponent", "positionChanged", "openSettings", "removed", "valueSettings", "textSaved", "valuesRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11216
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportCanvasComponent, isStandalone: true, selector: "app-report-canvas", inputs: { report: "report", editMode: "editMode", pageId: "pageId" }, outputs: { componentSettings: "componentSettings", reportChanged: "reportChanged", pageChanged: "pageChanged" }, viewQueries: [{ propertyName: "zoomPan", first: true, predicate: FsZoomPanComponent, descendants: true }, { propertyName: "_viewport", first: true, predicate: ["viewport"], descendants: true, static: true }, { propertyName: "_guides", first: true, predicate: ["guides"], descendants: true }, { propertyName: "_renderedComponents", predicate: ReportComponentComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [style.height.px]=\"bands.headerHeight * PX\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [style.height.px]=\"bands.footerHeight * PX\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\n <div class=\"page-empty\">\n Ask the assistant to add a chart, list or KPI to this page.\n </div>\n }\n </div>\n }\n </fs-zoom-pan>\n </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\n }\n <button\n type=\"button\"\n class=\"page-tab add-page\"\n matTooltip=\"Add page\"\n (click)=\"addPage()\">\n <mat-icon>add</mat-icon>\n Page\n </button>\n </div>\n\n <div class=\"zoom-controls\">\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom out (Ctrl/Cmd + scroll)\"\n (click)=\"zoomOut()\">\n <mat-icon>remove</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-label\"\n matTooltip=\"Zoom to 100%\"\n (click)=\"zoomActual()\">\n {{ (zoom() * 100).toFixed(0) }}%\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom in (Ctrl/Cmd + scroll)\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-width:0;min-height:0;flex:1;margin-top:10px}.canvas{display:flex;flex-direction:column;gap:8px;flex:1;min-height:0}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px}.canvas-toolbar .page-tabs{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;cursor:pointer}.canvas-toolbar .page-tabs .page-tab.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.canvas-toolbar .page-tabs .page-tab .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 8px;border-style:dashed;color:#52606d}.canvas-toolbar .page-tabs .page-tab.add-page mat-icon{font-size:16px;width:16px;height:16px}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.canvas-toolbar .zoom-controls{display:flex;align-items:center;gap:2px;margin-left:auto;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .zoom-controls .zoom-button{display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-button:hover{background:#f0f4f8}.canvas-toolbar .zoom-controls .zoom-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .zoom-controls .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-label:hover{color:#1f2933}.page-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.page-viewport{position:relative;flex:1;min-height:420px;border:1px solid #e4e7eb;border-radius:8px;background:#eef1f5;overflow:hidden}.page-viewport fs-zoom-pan{width:100%;height:100%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.guides{position:absolute;inset:0;pointer-events:none;z-index:30}.guides ::ng-deep .guide{position:absolute;background:#2196f3}.guides ::ng-deep .guide.guide-v{top:0;bottom:0;width:calc(1.25px / var(--canvas-zoom, 1))}.guides ::ng-deep .guide.guide-h{left:0;right:0;height:calc(1.25px / var(--canvas-zoom, 1))}.page-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;font-size:14px;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$3.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$3.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "ngmodule", type: FsZoomPanModule }, { kind: "component", type: i3$1.FsZoomPanComponent, selector: "fs-zoom-pan", inputs: ["zoomMax", "zoomMin", "zoomScale", "zoomDefault", "zoomFactor", "zoomFit", "fitMode", "fitPosition", "fitPadding", "fitMaxScale", "wheel", "top", "left"], outputs: ["moved", "zoomed"] }, { kind: "component", type: ReportComponentComponent, selector: "app-report-component", inputs: ["reportId", "component", "groups", "editMode", "zoom", "snapper", "selected", "tokens", "valueKeysElsewhere"], outputs: ["selectComponent", "positionChanged", "openSettings", "removed", "valueSettings", "textSaved", "valuesRemoved"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11898
11217
  }
11899
11218
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportCanvasComponent, decorators: [{
11900
11219
  type: Component,
@@ -11905,7 +11224,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
11905
11224
  FsMenuModule,
11906
11225
  FsZoomPanModule,
11907
11226
  ReportComponentComponent,
11908
- ], template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id + ':' + appliedVersion()) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [style.height.px]=\"bands.headerHeight * PX\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [style.height.px]=\"bands.footerHeight * PX\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id + ':' + appliedVersion()) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\n <div class=\"page-empty\">\n Ask the assistant to add a chart, list or KPI to this page.\n </div>\n }\n </div>\n }\n </fs-zoom-pan>\n </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\n }\n <button\n type=\"button\"\n class=\"page-tab add-page\"\n matTooltip=\"Add page\"\n (click)=\"addPage()\">\n <mat-icon>add</mat-icon>\n Page\n </button>\n </div>\n\n <div class=\"zoom-controls\">\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom out (Ctrl/Cmd + scroll)\"\n (click)=\"zoomOut()\">\n <mat-icon>remove</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-label\"\n matTooltip=\"Zoom to 100%\"\n (click)=\"zoomActual()\">\n {{ (zoom() * 100).toFixed(0) }}%\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom in (Ctrl/Cmd + scroll)\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-width:0;min-height:0;flex:1;margin-top:10px}.canvas{display:flex;flex-direction:column;gap:8px;flex:1;min-height:0}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px}.canvas-toolbar .page-tabs{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;cursor:pointer}.canvas-toolbar .page-tabs .page-tab.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.canvas-toolbar .page-tabs .page-tab .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 8px;border-style:dashed;color:#52606d}.canvas-toolbar .page-tabs .page-tab.add-page mat-icon{font-size:16px;width:16px;height:16px}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.canvas-toolbar .zoom-controls{display:flex;align-items:center;gap:2px;margin-left:auto;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .zoom-controls .zoom-button{display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-button:hover{background:#f0f4f8}.canvas-toolbar .zoom-controls .zoom-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .zoom-controls .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-label:hover{color:#1f2933}.page-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.page-viewport{position:relative;flex:1;min-height:420px;border:1px solid #e4e7eb;border-radius:8px;background:#eef1f5;overflow:hidden}.page-viewport fs-zoom-pan{width:100%;height:100%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.guides{position:absolute;inset:0;pointer-events:none;z-index:30}.guides ::ng-deep .guide{position:absolute;background:#2196f3}.guides ::ng-deep .guide.guide-v{top:0;bottom:0;width:calc(1.25px / var(--canvas-zoom, 1))}.guides ::ng-deep .guide.guide-h{left:0;right:0;height:calc(1.25px / var(--canvas-zoom, 1))}.page-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;font-size:14px;pointer-events:none}\n"] }]
11227
+ ], template: "<div class=\"canvas\">\n <div\n #viewport\n class=\"page-viewport\"\n (pointerdown)=\"onCanvasPointerDown()\">\n <fs-zoom-pan\n wheel=\"modifier\"\n [zoomMin]=\"0.1\"\n [zoomMax]=\"3\"\n (zoomed)=\"onZoomed($event)\">\n @if (activePage) {\n <div\n class=\"page\"\n [class.editing]=\"editMode\"\n [class.master-editing]=\"isMasterEditing\"\n [style.width.px]=\"report.pageWidth * PX\"\n [style.height.px]=\"report.pageHeight * PX\"\n [style.--canvas-zoom]=\"zoom()\"\n [style.--report-heading-size.px]=\"headingSizePx\">\n <!-- The header and footer: the master's components, drawn under the\n page's own. -->\n @if (isMasterShown && masterComponents().length) {\n <div\n class=\"master-layer\"\n [class.editable]=\"isMasterEditing\">\n @for (component of masterComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isMasterEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"masterTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n </div>\n }\n\n <!-- The header and footer bands, as guides while editing. -->\n @if (editMode) {\n @if (bands.headerHeight) {\n <div\n class=\"band band-header\"\n [style.height.px]=\"bands.headerHeight * PX\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Header</span>\n }\n </div>\n }\n @if (bands.footerHeight) {\n <div\n class=\"band band-footer\"\n [style.height.px]=\"bands.footerHeight * PX\">\n @if (isMasterEditing) {\n <span class=\"band-label small\">Footer</span>\n }\n </div>\n }\n }\n\n @for (component of activePageComponents(); track component.id) {\n <app-report-component\n [reportId]=\"report.id\"\n [component]=\"component\"\n [groups]=\"groups\"\n [editMode]=\"isPageEditing\"\n [zoom]=\"zoom()\"\n [snapper]=\"snapper\"\n [tokens]=\"pageTokens()\"\n [valueKeysElsewhere]=\"valueKeysElsewhere\"\n [selected]=\"component.id === selectedComponentId()\"\n [style.left.px]=\"component.x * PX\"\n [style.top.px]=\"component.y * PX\"\n [style.width.px]=\"component.w * PX\"\n [style.height.px]=\"component.autoHeight ? null : component.h * PX\"\n (selectComponent)=\"selectComponent($event)\"\n (positionChanged)=\"onPositionChanged($event)\"\n (removed)=\"onComponentRemoved($event)\"\n (openSettings)=\"componentSettings.emit($event)\"\n (valueSettings)=\"openValueSettings(component, $event)\"\n (textSaved)=\"onTextSaved(component, $event)\"\n (valuesRemoved)=\"reportChanged.emit()\">\n </app-report-component>\n }\n\n <div\n #guides\n class=\"guides\">\n </div>\n\n @if (!activePageComponents().length && !masterMode()) {\n <div class=\"page-empty\">\n Ask the assistant to add a chart, list or KPI to this page.\n </div>\n }\n </div>\n }\n </fs-zoom-pan>\n </div>\n\n <div class=\"canvas-toolbar\">\n <!-- Tabs are one horizontal drop list: dragging a chip reorders the pages.\n The drop INDEX is computed along X (orientation), but the preview is\n deliberately left unbounded on both axes \u2014 pinning it to the row with\n cdkDragBoundary means the chip you are dragging covers the chips you\n are dragging it past, and you cannot see where it is going to land.\n Every page gets a chip, including a lone one: the chip is what tells\n you which page you are on, and a report that hid it left you unable to\n tell page 1 of 1 from a page that failed to load. Deleting the last\n page is refused server-side, so the kebab is safe to expose here. -->\n <div\n class=\"page-tabs\"\n cdkDropList\n cdkDropListOrientation=\"horizontal\"\n (cdkDropListDropped)=\"onPageDropped($event)\">\n @for (page of pages(); track page.id; let index = $index) {\n <!-- The chip is the drag handle AND the tab: clicking selects,\n dragging reorders. The kebab lives inside it, so the whole\n control stays one chip instead of growing a second button. -->\n <div\n cdkDrag\n class=\"page-tab\"\n [class.active]=\"index === activePageIndex()\"\n (click)=\"selectPage(index)\">\n <span class=\"page-tab-label\">{{ pageLabel(page, index) }}</span>\n <!-- The kebab opens the menu rather than starting a drag or\n switching pages, so it swallows its own click. -->\n <span\n class=\"page-tab-menu\"\n [fsMenuTriggerFor]=\"pageMenu\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\">\n <mat-icon>more_vert</mat-icon>\n </span>\n </div>\n <!-- Outside the chip: cdkDrag clones its host into the drag preview,\n and a menu overlay anchored to a cloned trigger opens detached\n from the real tab. -->\n <fs-menu #pageMenu>\n <ng-template\n fs-menu-item\n (click)=\"renamePage(page)\">\n Edit name\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleMasterHidden(page)\">\n {{ page.masterHidden ? 'Show header & footer' : 'Hide header & footer' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"deletePage(page)\">\n Delete\n </ng-template>\n </fs-menu>\n }\n <button\n type=\"button\"\n class=\"page-tab add-page\"\n matTooltip=\"Add page\"\n (click)=\"addPage()\">\n <mat-icon>add</mat-icon>\n Page\n </button>\n </div>\n\n <div class=\"zoom-controls\">\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom out (Ctrl/Cmd + scroll)\"\n (click)=\"zoomOut()\">\n <mat-icon>remove</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-label\"\n matTooltip=\"Zoom to 100%\"\n (click)=\"zoomActual()\">\n {{ (zoom() * 100).toFixed(0) }}%\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Zoom in (Ctrl/Cmd + scroll)\"\n (click)=\"zoomIn()\">\n <mat-icon>add</mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"zoom-button\"\n matTooltip=\"Fit page\"\n (click)=\"zoomFit()\">\n <mat-icon>fit_screen</mat-icon>\n </button>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-width:0;min-height:0;flex:1;margin-top:10px}.canvas{display:flex;flex-direction:column;gap:8px;flex:1;min-height:0}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px}.canvas-toolbar .page-tabs{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.canvas-toolbar .page-tabs fs-menu{display:none}.canvas-toolbar .page-tabs .page-tab{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;cursor:pointer}.canvas-toolbar .page-tabs .page-tab.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.canvas-toolbar .page-tabs .page-tab .page-tab-label{white-space:nowrap}.canvas-toolbar .page-tabs .page-tab .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.canvas-toolbar .page-tabs .page-tab .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.canvas-toolbar .page-tabs .page-tab .page-tab-menu:hover{background:#0f172a14;opacity:1}.canvas-toolbar .page-tabs .page-tab.active .page-tab-menu:hover{background:#ffffff38}.canvas-toolbar .page-tabs .page-tab.add-page{gap:2px;padding:4px 14px 4px 8px;border-style:dashed;color:#52606d}.canvas-toolbar .page-tabs .page-tab.add-page mat-icon{font-size:16px;width:16px;height:16px}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder{background:transparent;border-style:dashed;border-color:#cbd2d9}.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-label,.canvas-toolbar .page-tabs .page-tab.cdk-drag-placeholder .page-tab-menu{visibility:hidden}.canvas-toolbar .page-tabs .cdk-drag-animating,.canvas-toolbar .page-tabs.cdk-drop-list-dragging .page-tab:not(.cdk-drag-placeholder){transition:transform .18s cubic-bezier(0,0,.2,1)}.canvas-toolbar .zoom-controls{display:flex;align-items:center;gap:2px;margin-left:auto;border:1px solid #e4e7eb;border-radius:18px;background:#fff;padding:2px 6px}.canvas-toolbar .zoom-controls .zoom-button{display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;color:#52606d;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-button:hover{background:#f0f4f8}.canvas-toolbar .zoom-controls .zoom-button mat-icon{font-size:18px;width:18px;height:18px}.canvas-toolbar .zoom-controls .zoom-label{border:none;background:transparent;font-size:12px;color:#52606d;min-width:44px;cursor:pointer}.canvas-toolbar .zoom-controls .zoom-label:hover{color:#1f2933}.page-tab.cdk-drag-preview{display:inline-flex;align-items:center;box-sizing:border-box;border:1px solid #e4e7eb;border-radius:16px;background:#fff;padding:4px 6px 4px 14px;font-size:12px;line-height:16px;color:#52606d;box-shadow:0 3px 10px #0f172a3d}.page-tab.cdk-drag-preview.active{background:var(--brand-primary-color, #3b82f6);border-color:var(--brand-primary-color, #3b82f6);color:#fff}.page-tab.cdk-drag-preview .page-tab-label{white-space:nowrap}.page-tab.cdk-drag-preview .page-tab-menu{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:16px;height:16px;margin-left:2px;border-radius:50%;opacity:.75}.page-tab.cdk-drag-preview .page-tab-menu mat-icon{font-size:14px;width:14px;height:14px;line-height:14px}.page-viewport{position:relative;flex:1;min-height:420px;border:1px solid #e4e7eb;border-radius:8px;background:#eef1f5;overflow:hidden}.page-viewport fs-zoom-pan{width:100%;height:100%}.page{position:relative;background:#fff;border-radius:2px;box-shadow:0 2px 12px #0f172a1f,0 0 0 1px #0f172a0a;box-sizing:border-box}.page.editing{background-image:radial-gradient(circle,#d8dee6 1px,transparent 1px);background-size:24px 24px}.page.master-editing>app-report-component{opacity:.35;pointer-events:none}.master-layer{position:absolute;inset:0;pointer-events:none}.master-layer.editable{z-index:10}.master-layer.editable app-report-component{pointer-events:auto}.band{position:absolute;left:0;right:0;z-index:5;pointer-events:none;background:#2196f30a;border-color:#2196f373;border-style:dashed;border-width:0}.band.band-header{top:0;border-bottom-width:calc(1px / var(--canvas-zoom, 1))}.band.band-footer{bottom:0;border-top-width:calc(1px / var(--canvas-zoom, 1))}.band .band-label{position:absolute;top:calc(10px / var(--canvas-zoom, 1));left:calc(10px / var(--canvas-zoom, 1));transform:scale(calc(1 / var(--canvas-zoom, 1)));transform-origin:top left;color:#2196f3cc;font-weight:600;text-transform:uppercase;letter-spacing:.06em}.guides{position:absolute;inset:0;pointer-events:none;z-index:30}.guides ::ng-deep .guide{position:absolute;background:#2196f3}.guides ::ng-deep .guide.guide-v{top:0;bottom:0;width:calc(1.25px / var(--canvas-zoom, 1))}.guides ::ng-deep .guide.guide-h{left:0;right:0;height:calc(1.25px / var(--canvas-zoom, 1))}.page-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#9aa5b1;font-size:14px;pointer-events:none}\n"] }]
11909
11228
  }], ctorParameters: () => [], propDecorators: { report: [{
11910
11229
  type: Input
11911
11230
  }], editMode: [{
@@ -13377,213 +12696,6 @@ function nameInputId(groupId) {
13377
12696
  return `filterName${groupId}`;
13378
12697
  }
13379
12698
 
13380
- // The open report's saved views, per open report beside the state service:
13381
- // the list the viewer may see, the view that is open, the order a report
13382
- // opens in and the calls that change a view. The viewer state stays
13383
- // ReportFilterStateService's: this service puts a view on screen through
13384
- // apply() and reads the screen through snapshot(), base and viewChanged.
13385
- // Nothing here runs for a report whose server does not list 'views'
13386
- // (serves()). A view is opened from the Views menu only: a report keeps no
13387
- // state in the browser URL, so there is no link to a view.
13388
- //
13389
- // The order a report opens in (open(), on its first load only):
13390
- // 1. The viewer's default view.
13391
- // 2. Else the report's authored defaults.
13392
- // A ?page= the report opened with beats the default view's page.
13393
- class ReportViewService {
13394
- // The views the viewer may see, names and flags only: its own first, then
13395
- // the ones others share. Empty until the list is read.
13396
- views = computed(() => this._views());
13397
- // The viewer may share its views with everyone.
13398
- canShare = computed(() => this._canShare());
13399
- // The open view, with its config as it was read or saved; null while the
13400
- // screen is on the report's defaults.
13401
- openView = computed(() => this._openView());
13402
- // Whether the screen differs from the open view; false with none open.
13403
- viewChanged = computed(() => !!this._openView() && this._filterState.viewChanged());
13404
- _reportData = inject(ReportData);
13405
- _filterState = inject(ReportFilterStateService);
13406
- _message = inject(FsMessage);
13407
- _destroyRef = inject(DestroyRef);
13408
- _views = signal([]);
13409
- _canShare = signal(false);
13410
- _openView = signal(null);
13411
- // The report the views belong to.
13412
- _reportId = null;
13413
- // Whether the open report's server keeps saved views.
13414
- serves(report) {
13415
- return !!report?.viewerFeatures?.includes('views');
13416
- }
13417
- // A report's first load, in the open order above: the state service is
13418
- // set up for `report` (init) once the list is read, and the screen opened
13419
- // on the viewer's default view, else on the report's defaults. `pageId` is
13420
- // the ?page= the report opened with, which beats the view's page. Ends once
13421
- // the screen is set.
13422
- open(report, pageId) {
13423
- this._start(report);
13424
- return this._readViews(report)
13425
- .pipe(tap((list) => {
13426
- this._filterState.init(report);
13427
- if (list?.myDefault) {
13428
- this._openOn(list.myDefault, () => this._filterState.apply(list.myDefault.config, pageId));
13429
- }
13430
- }), map(() => undefined));
13431
- }
13432
- // A later load of the open report (a chat edit, a settings save): the
13433
- // screen and the open view stay, and the list is read again behind them so
13434
- // it names what the server holds now.
13435
- refresh(report) {
13436
- this._start(report);
13437
- this._readViews(report)
13438
- .pipe(takeUntilDestroyed(this._destroyRef))
13439
- .subscribe();
13440
- }
13441
- // Opens a view picked from the list; its config is read now.
13442
- pick(reportViewId) {
13443
- return this._reportData.view(this._reportId, reportViewId)
13444
- .pipe(tap((view) => this._openOn(view, () => this._filterState.apply(view.config))));
13445
- }
13446
- // Closes the open view and puts the screen on the report's defaults.
13447
- openDefaults() {
13448
- this._openView.set(null);
13449
- this._filterState.apply(null);
13450
- }
13451
- // Saves the screen as a new view, which becomes the open view. The screen
13452
- // is already the view, so nothing is applied again.
13453
- saveAsNew(view) {
13454
- return this._reportData.createView(this._reportId, { ...view, config: this._filterState.snapshot() })
13455
- .pipe(tap((saved) => {
13456
- this._openView.set(saved);
13457
- this._putRow(saved);
13458
- this._filterState.setBaseToScreen();
13459
- this._message.success('View saved');
13460
- }));
13461
- }
13462
- // Saves the screen over the open view, keeping the keys this viewer does
13463
- // not know (withStoredKeys).
13464
- saveChanges() {
13465
- const open = this._openView();
13466
- if (!open) {
13467
- return EMPTY;
13468
- }
13469
- return this._reportData.updateView(this._reportId, open.id, { config: withStoredKeys(open.config, this._filterState.snapshot()) })
13470
- .pipe(tap((saved) => {
13471
- this._openView.set(saved);
13472
- this._putRow(saved);
13473
- this._filterState.setBaseToScreen();
13474
- this._message.success('View saved');
13475
- }));
13476
- }
13477
- rename(name) {
13478
- return this._changeOpen((reportViewId) => this._reportData.updateView(this._reportId, reportViewId, { name }));
13479
- }
13480
- // Shares the open view with everyone who may open the report, or stops.
13481
- share(shared) {
13482
- return this._changeOpen((reportViewId) => this._reportData.shareView(this._reportId, reportViewId, shared));
13483
- }
13484
- // Makes the open view the viewer's default, or stops opening it by default.
13485
- setDefault(personalDefault) {
13486
- return this._changeOpen((reportViewId) => this._reportData.setViewDefault(this._reportId, reportViewId, personalDefault));
13487
- }
13488
- // Deletes the open view. The screen stays; the report's defaults become
13489
- // what it is compared with.
13490
- deleteOpen() {
13491
- const open = this._openView();
13492
- if (!open) {
13493
- return EMPTY;
13494
- }
13495
- return this._reportData.deleteView(this._reportId, open.id)
13496
- .pipe(tap(() => {
13497
- this._openView.set(null);
13498
- this._views.update((views) => views.filter((view) => view.id !== open.id));
13499
- this._filterState.clearBase();
13500
- this._message.success('View deleted');
13501
- }));
13502
- }
13503
- // Starts over for another report: no list and no open view.
13504
- _start(report) {
13505
- if (report.id === this._reportId) {
13506
- return;
13507
- }
13508
- this._reportId = report.id;
13509
- this._views.set([]);
13510
- this._canShare.set(false);
13511
- this._openView.set(null);
13512
- }
13513
- // Makes `view` the open view, puts it on screen (`applyView`), and says
13514
- // how many of its settings no longer apply (and to save it, when the
13515
- // viewer may).
13516
- _openOn(view, applyView) {
13517
- this._openView.set(view);
13518
- applyView();
13519
- const note = viewDroppedMessage(viewDroppedCount(view, this._filterState.base()), view.canChange);
13520
- if (note) {
13521
- this._message.warning(note);
13522
- }
13523
- }
13524
- // Runs a change on the open view and takes its answer as the open view
13525
- // and its row in the list.
13526
- _changeOpen(change) {
13527
- const open = this._openView();
13528
- if (!open) {
13529
- return EMPTY;
13530
- }
13531
- return change(open.id)
13532
- .pipe(tap((view) => {
13533
- this._openView.set(view);
13534
- this._putRow(view);
13535
- }));
13536
- }
13537
- // A view's row in the list, replaced or added. A new default clears the
13538
- // flag on the viewer's other views, as the server does. A new row keeps
13539
- // the list's order: the viewer's own views first, then the ones others
13540
- // share.
13541
- _putRow(view) {
13542
- const summary = viewSummary(view);
13543
- this._views.update((views) => {
13544
- const rows = views.map((row) => (view.personalDefault && row.personalDefault ? { ...row, personalDefault: false } : row));
13545
- const index = rows.findIndex((row) => row.id === view.id);
13546
- if (index >= 0) {
13547
- return rows.map((row, at) => (at === index ? summary : row));
13548
- }
13549
- const othersAt = view.mine ? rows.findIndex((row) => !row.mine) : -1;
13550
- return othersAt < 0
13551
- ? [...rows, summary]
13552
- : [...rows.slice(0, othersAt), summary, ...rows.slice(othersAt)];
13553
- });
13554
- }
13555
- // The list, read for `report`; null when it cannot be read. Its answer is
13556
- // kept only while the report is still the open one.
13557
- _readViews(report) {
13558
- return this._reportData.views(report.id, { context: new HttpContext().set(DisplayApiError, false) })
13559
- .pipe(catchError(() => of(null)), tap((list) => {
13560
- if (list && report.id === this._reportId) {
13561
- this._views.set(list.views ?? []);
13562
- this._canShare.set(!!list.canShare);
13563
- }
13564
- }));
13565
- }
13566
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
13567
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewService });
13568
- }
13569
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewService, decorators: [{
13570
- type: Injectable
13571
- }] });
13572
- // A view's row as the list holds it: names and flags, no config.
13573
- function viewSummary(view) {
13574
- return {
13575
- id: view.id,
13576
- name: view.name,
13577
- shared: view.shared,
13578
- mine: view.mine,
13579
- personalDefault: view.personalDefault,
13580
- ownerName: view.ownerName,
13581
- canChange: view.canChange,
13582
- canDelete: view.canDelete,
13583
- modifyDate: view.modifyDate,
13584
- };
13585
- }
13586
-
13587
12699
  // How far a KPI card's rows shrink to fit a short tile. A tile shorter still
13588
12700
  // lets the card run past its foot rather than print it unreadably small.
13589
12701
  const KPI_EXPORT_MIN_SCALE = 0.5;
@@ -13856,20 +12968,18 @@ function showsFailedValue(components, failed) {
13856
12968
  .flatMap((component) => valueKeys(component.config?.html)));
13857
12969
  return failed.some((key) => shown.has(key));
13858
12970
  }
13859
- // The cover's filter lines: the open view, the export's filter lines, then
13860
- // the match while the report bar's filters match any. A drilled chart's line
12971
+ // The cover's filter lines: the export's filter lines, then the match while the report bar's filters match any. A drilled chart's line
13861
12972
  // is its title as the canvas header reads it (`titles`, by component id) and
13862
12973
  // the crumbs of the path its answer applied (the ones its title prints), not
13863
12974
  // the path the screen asked for: the server may have cut that path short or
13864
12975
  // refused it.
13865
12976
  function coverFilterLines(snapshot, lines, pages, titles = {}) {
13866
- const match = snapshot.features.includes('matchAny') ? matchLineText(snapshot.view.match, 'line') : null;
12977
+ const match = snapshot.features.includes('matchAny') ? matchLineText(snapshot.state.match, 'line') : null;
13867
12978
  const drilled = pages
13868
12979
  .flatMap((page) => page.components)
13869
12980
  .filter((item) => !!item.crumbs?.length)
13870
12981
  .map((item) => crumbedTitle(titles[item.component.id] ?? item.component.title, item.crumbs));
13871
12982
  return [
13872
- snapshot.viewLine,
13873
12983
  ...lines.filter((line) => line.kind !== 'drill').map((line) => line.text),
13874
12984
  ...drilled,
13875
12985
  match,
@@ -13896,9 +13006,6 @@ class ReportExportCollectorService {
13896
13006
  _reportData = inject(ReportData);
13897
13007
  _filterState = inject(ReportFilterStateService);
13898
13008
  _filterOptions = inject(ReportFilterOptionsService);
13899
- // The open saved view, for the cover's view line. Only a view that keeps
13900
- // saved views provides it.
13901
- _views = inject(ReportViewService, { optional: true });
13902
13009
  // listRows is how many rows each list fetches: the renderer's own cap, since
13903
13010
  // a PDF table continues onto extra pages and a slide table cannot.
13904
13011
  //
@@ -13936,30 +13043,29 @@ class ReportExportCollectorService {
13936
13043
  };
13937
13044
  }));
13938
13045
  }
13939
- // The screen as one export reads it: the view document, each group's value
13046
+ // The screen as one export reads it: the state snapshot, each group's value
13940
13047
  // and live preset, each tile's filter entries and state (tileDataState),
13941
- // each value's filter entries, the grain, the features served, the format
13942
- // and the open view's line. A value's entries are read here too, so the
13048
+ // each value's filter entries, the grain, the features served and the
13049
+ // format. A value's entries are read here too, so the
13943
13050
  // numbers in a file's text are filtered as its tiles are, whatever the bar
13944
13051
  // does while the export runs.
13945
13052
  snapshot(report) {
13946
- const view = this._filterState.snapshot();
13053
+ const state = this._filterState.snapshot();
13947
13054
  const tiles = (report.pages ?? [])
13948
13055
  .flatMap((page) => page.components ?? [])
13949
13056
  .filter((component) => isTile(component));
13950
13057
  const values = reportComponents(report)
13951
13058
  .filter((component) => component.type === 'value');
13952
13059
  return {
13953
- view,
13060
+ state,
13954
13061
  ...this._groupValues(report),
13955
13062
  filters: Object.fromEntries(tiles.map((tile) => [tile.id, this._filterState.resolveForComponent(tile)])),
13956
13063
  valueFilters: Object.fromEntries(values
13957
13064
  .map((value) => [value.id, this._filterState.resolveForComponent(value)])),
13958
- states: Object.fromEntries(tiles.map((tile) => [tile.id, tileDataState(report, tile, view)])),
13065
+ states: Object.fromEntries(tiles.map((tile) => [tile.id, tileDataState(report, tile, state)])),
13959
13066
  frequency: this._filterState.frequency(),
13960
13067
  features: offeredViewerFeatures(report.viewerFeatures),
13961
13068
  formatContext: this._filterState.formatContext(),
13962
- viewLine: viewLineText(this._views?.openView(), !!this._views?.viewChanged()),
13963
13069
  };
13964
13070
  }
13965
13071
  // How an export words the screen, over a snapshot (`pageId` for a This
@@ -14108,7 +13214,7 @@ class ReportExportCollectorService {
14108
13214
  _pageValues(report, pageId, values, shown, snapshot) {
14109
13215
  const formatContext = snapshot.formatContext;
14110
13216
  const filters = values.flatMap((component) => snapshot.valueFilters[component.id] ?? []);
14111
- const matchAnyGroupIds = valuesMatchAnyGroupIds(report, pageId, snapshot.view);
13217
+ const matchAnyGroupIds = valuesMatchAnyGroupIds(report, pageId, snapshot.state);
14112
13218
  return this._reportData.pageValues(report.id, pageId, filters, true, { handlers: false }, matchAnyGroupIds)
14113
13219
  .pipe(map$1((rows) => new Map((rows ?? []).map((row) => [row.id, row]))), catchError(() => of(new Map())), map$1((rows) => ({
14114
13220
  values: textValues(values, { rows }, formatContext),
@@ -14202,7 +13308,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
14202
13308
  // in the report's format context, as on screen.
14203
13309
  async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
14204
13310
  // The same tree-shaken echarts build + 'report' theme the live canvas uses.
14205
- const echarts = (await import('./firestitch-report-echarts-CyHkFToO.mjs')).default;
13311
+ const echarts = (await import('./firestitch-report-echarts-DKXlgu1D.mjs')).default;
14206
13312
  const host = document.createElement('div');
14207
13313
  host.style.width = `${width}px`;
14208
13314
  host.style.height = `${height}px`;
@@ -16850,7 +15956,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
16850
15956
  // presets (Today, Last 7 days, This month, ...). The report adds none of its
16851
15957
  // own: the built-in list covers every period a report filter names. A preset
16852
15958
  // is a shortcut only: picking one sets the range's two days, which is all the
16853
- // bar and a saved view hold. Provided on the view; the host app's
15959
+ // bar and a saved filter hold. Provided on the view; the host app's
16854
15960
  // own pickers keep their config.
16855
15961
  function provideReportDatePicker() {
16856
15962
  const config = {
@@ -16953,9 +16059,9 @@ class ReportComponent {
16953
16059
  _pageId = signal(null);
16954
16060
  constructor() {
16955
16061
  this._buildReportConfig();
16956
- // apply() and resetToBase() move the state version. The bar is built
16957
- // again for it, and reportFilterKey, which carries it, recreates
16958
- // fs-filter: it reads its config once.
16062
+ // resetToDefaults() moves the state version. The bar is built again for
16063
+ // it, and reportFilterKey, which carries it, recreates fs-filter: it reads
16064
+ // its config once.
16959
16065
  effect(() => {
16960
16066
  const version = this._filterState.stateVersion();
16961
16067
  untracked(() => {
@@ -17133,10 +16239,10 @@ class ReportComponent {
17133
16239
  // notes get the filter lines as the cover words them (exportLines; a This
17134
16240
  // Page zip names only the groups that reach the page), and each file is
17135
16241
  // named after its tile's title as the canvas header reads it (titles). All
17136
- // of it is read once, when the export starts (exportWording). This view
17137
- // keeps no saved views, so it names none. `thisPage` narrows it to the page
17138
- // the canvas is showing. Unlike the PDF/PowerPoint exports the files are
17139
- // written server-side, so they are not capped by what a slide can hold.
16242
+ // of it is read once, when the export starts (exportWording). `thisPage`
16243
+ // narrows it to the page the canvas is showing. Unlike the PDF/PowerPoint
16244
+ // exports the files are written server-side, so they are not capped by
16245
+ // what a slide can hold.
17140
16246
  exportData(thisPage = false) {
17141
16247
  const report = this.report();
17142
16248
  if (!report) {
@@ -17146,9 +16252,9 @@ class ReportComponent {
17146
16252
  const filters = this._filterState.resolveForReport(report, pageId);
17147
16253
  const frequency = this._filterState.frequency();
17148
16254
  const snapshot = this._collector.snapshot(report);
17149
- const states = zipExportStates(report, snapshot.view, pageId);
16255
+ const states = zipExportStates(report, snapshot.state, pageId);
17150
16256
  this._process.download('Exporting data', this._collector.exportWording(report, snapshot, pageId)
17151
- .pipe(switchMap(({ lines, titles }) => this._reportData.exportReport(report.id, filters, pageId, frequency, lines, states, null, false, titles))));
16257
+ .pipe(switchMap(({ lines, titles }) => this._reportData.exportReport(report.id, filters, pageId, frequency, lines, states, titles))));
17152
16258
  }
17153
16259
  // React to each chat turn (fs-chat handles the request + persistence); we
17154
16260
  // only care about the report side effects this context's backend flags.
@@ -17274,14 +16380,12 @@ class ReportComponent {
17274
16380
  persist: false,
17275
16381
  items,
17276
16382
  change: (query) => this._barChanged(query ?? {}, !!matchItem),
17277
- // 'Clear filters' puts the screen back on the report's defaults (this
17278
- // view opens no saved views) and drops the temporary layer. A server
17279
- // without saved views keeps the clear it always had: the bar's items
17280
- // only.
17281
- clear: features.includes('views') ? () => this._filterState.resetToBase() : undefined,
16383
+ // 'Clear filters' puts the screen back on the report's authored
16384
+ // defaults and drops the temporary layer.
16385
+ clear: () => this._filterState.resetToDefaults(),
17282
16386
  // The chip shows exactly while the screen differs from the defaults a
17283
16387
  // clear returns to (C5).
17284
- clearVisible: features.includes('views') ? () => this._filterState.viewChanged() : undefined,
16388
+ clearVisible: () => this._filterState.differsFromDefaults(),
17285
16389
  });
17286
16390
  }
17287
16391
  // The bar's values into the state: every group it shows, the grain and,
@@ -17337,7 +16441,7 @@ class ReportComponent {
17337
16441
  ReportExportService,
17338
16442
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
17339
16443
  // this route's chunk.
17340
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CyHkFToO.mjs').then((module) => module.default) }),
16444
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DKXlgu1D.mjs').then((module) => module.default) }),
17341
16445
  // The bar's date pickers start their weeks on Monday and list the built-in
17342
16446
  // presets; a preset only sets the days.
17343
16447
  provideReportDatePicker(),
@@ -17358,7 +16462,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
17358
16462
  ReportExportService,
17359
16463
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
17360
16464
  // this route's chunk.
17361
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CyHkFToO.mjs').then((module) => module.default) }),
16465
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DKXlgu1D.mjs').then((module) => module.default) }),
17362
16466
  // The bar's date pickers start their weeks on Monday and list the built-in
17363
16467
  // presets; a preset only sets the days.
17364
16468
  provideReportDatePicker(),
@@ -18547,260 +17651,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
18547
17651
  ], template: "<fs-filter [config]=\"filterConfig\"></fs-filter>\n\n<ng-container *fsSkeleton=\"nodes()\">\n <fs-tree [config]=\"treeConfig\">\n <ng-template\n fsTreeNode\n let-data=\"data\">\n <div\n class=\"fs-row.gap-sm.align-center node-row\"\n [class.deleted]=\"data.deleted\">\n <mat-icon class=\"node-icon\">\n {{ data.type === nodeType.Folder ? 'folder' : 'insert_chart_outlined' }}\n </mat-icon>\n\n @if (data.type === nodeType.Folder || data.deleted) {\n <!-- A deleted report can't be opened (the backend only assembles\n live ones, and one under a deleted folder is not on the live\n listing), so in Show Deleted mode the name is plain text \u2014\n restore it from the row menu first. -->\n <span class=\"node-name\">{{ data.name }}</span>\n } @else {\n <a\n class=\"node-name\"\n [routerLink]=\"openLink(data.report)\">\n {{ data.name }}\n </a>\n }\n\n <!-- Only on the row somebody deleted. Everything under a deleted\n folder is muted with it, but was not deleted itself. -->\n @if (data.stateDeleted) {\n <fs-chip size=\"tiny\">Deleted</fs-chip>\n }\n </div>\n </ng-template>\n </fs-tree>\n\n @if (!nodes().length) {\n <p class=\"small tree-empty\">\n No reports yet. Create one, or add a folder to group them.\n </p>\n }\n</ng-container>\n", styles: ["a{cursor:pointer}fs-tree .node>.container{flex:1;min-width:0}fs-tree .node>.container>.content{min-width:0}fs-tree .node>.container>button{margin-top:-5px}.node-row{min-width:0}.node-row.deleted{opacity:.55}.node-icon{font-size:18px;width:18px;height:18px;flex:none}.node-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-empty{margin-top:10px}\n"] }]
18548
17652
  }], ctorParameters: () => [] });
18549
17653
 
18550
- const VIEWS_LABEL = 'Views';
18551
- const EDITED_TOOLTIP = 'The screen differs from this view';
18552
- const SAVE_AS_TITLE = 'Save as new view';
18553
- const RENAME_TITLE = 'Rename view';
18554
- const CROSS_FILTER_NOTE = 'Cross-filters are temporary and are not saved.';
18555
- const VIEW_NAME_BLANK = 'Enter a name';
18556
- // The server's limit on a view's name.
18557
- const VIEW_NAME_MAX_LENGTH = 100;
18558
- // The menu for the open view (null when none is open), the views the viewer
18559
- // may see, whether the screen differs from the open view, and whether the
18560
- // viewer may share its views.
18561
- function viewMenuItems(open, views, edited, canShare) {
18562
- const rows = [
18563
- ...views.filter((view) => view.mine),
18564
- ...views.filter((view) => !view.mine),
18565
- ].map((view) => viewRow(view, open, edited));
18566
- const actions = openViewActions(open, edited, canShare);
18567
- const resets = [
18568
- ...(open && edited ? [action('resetToView', `Reset to "${open.name}"`)] : []),
18569
- action('resetToDefaults', 'Reset to report defaults'),
18570
- ];
18571
- return [
18572
- ...rows,
18573
- ...withDivider(actions, rows.length > 0),
18574
- ...withDivider(resets, true),
18575
- ];
18576
- }
18577
- // The picker's label: 'Views', or the open view's name, marked while the
18578
- // screen differs from it.
18579
- function pickerLabel(open, edited) {
18580
- if (!open) {
18581
- return VIEWS_LABEL;
18582
- }
18583
- return edited ? `${open.name} (edited)` : open.name;
18584
- }
18585
- // The picker's tooltip: only while the screen differs from the open view.
18586
- function pickerTooltip(open, edited) {
18587
- return open && edited ? EDITED_TOOLTIP : null;
18588
- }
18589
- // The question before a view is deleted. A shared view says that everyone
18590
- // loses it.
18591
- function deletePromptText(view) {
18592
- const question = `Delete the view "${view.name}"?`;
18593
- return view.shared ? `${question} Everyone it is shared with loses it.` : question;
18594
- }
18595
- // The line the save dialog shows under the name, or null: a cross-filter is
18596
- // on, and it does not go into the view.
18597
- function saveDialogNote(hasCrossFilter) {
18598
- return hasCrossFilter ? CROSS_FILTER_NOTE : null;
18599
- }
18600
- // A name made of spaces only is no name; the server trims it.
18601
- function viewNameError(name) {
18602
- return (name ?? '').trim() ? null : VIEW_NAME_BLANK;
18603
- }
18604
- function viewRow(view, open, edited) {
18605
- const isOpen = view.id === open?.id;
18606
- return {
18607
- kind: 'view',
18608
- label: view.name,
18609
- disabled: isOpen && !edited,
18610
- tooltip: viewTooltip(view),
18611
- divider: false,
18612
- viewId: view.id,
18613
- open: isOpen,
18614
- icon: viewIcon(view),
18615
- };
18616
- }
18617
- // Who shared a view of someone else's; whether one of the viewer's own opens
18618
- // by default or is shared.
18619
- function viewTooltip(view) {
18620
- if (!view.mine) {
18621
- return view.ownerName ? `Shared by ${view.ownerName}` : 'Shared with everyone';
18622
- }
18623
- if (view.personalDefault && view.shared) {
18624
- return 'Opens by default, shared with everyone';
18625
- }
18626
- if (view.personalDefault) {
18627
- return 'Opens by default';
18628
- }
18629
- return view.shared ? 'Shared with everyone' : null;
18630
- }
18631
- // The open row keeps this icon too; its check is drawn beside it.
18632
- function viewIcon(view) {
18633
- if (!view.mine) {
18634
- return 'people';
18635
- }
18636
- return view.personalDefault ? 'star' : null;
18637
- }
18638
- // The rows that act on the open view; with none open, only Save as new view.
18639
- function openViewActions(open, edited, canShare) {
18640
- const items = [];
18641
- if (open?.canChange && edited) {
18642
- items.push(action('saveChanges', 'Save changes'));
18643
- }
18644
- items.push(action('saveAsNew', 'Save as new view...'));
18645
- if (open?.canChange) {
18646
- items.push(action('rename', 'Rename...'));
18647
- }
18648
- if (open?.mine) {
18649
- items.push(open.personalDefault
18650
- ? action('clearDefault', 'Stop opening it by default')
18651
- : action('setDefault', 'Make it my default'));
18652
- }
18653
- if (open?.mine && canShare) {
18654
- items.push(open.shared ? action('unshare', 'Stop sharing') : action('share', 'Share with everyone'));
18655
- }
18656
- if (open?.canDelete) {
18657
- items.push(action('delete', 'Delete...'));
18658
- }
18659
- return items;
18660
- }
18661
- function action(kind, label) {
18662
- return { kind, label, disabled: false, tooltip: null, divider: false, viewId: null, open: false, icon: null };
18663
- }
18664
- // The same rows, the first with a line above it when `divider` is set.
18665
- function withDivider(items, divider) {
18666
- return items.map((item, index) => (index === 0 && divider ? { ...item, divider: true } : item));
18667
- }
18668
-
18669
- // Names a saved view: Save as new view and Rename. One name field; the
18670
- // caller's save call does the work, and the dialog closes once it answers.
18671
- // A refusal from the server (a name taken, too many views) shows as the
18672
- // API's own error and leaves the dialog open.
18673
- class ReportViewDialogComponent {
18674
- data = inject(MAT_DIALOG_DATA);
18675
- name = signal(this.data.name);
18676
- nameMaxLength = VIEW_NAME_MAX_LENGTH;
18677
- _dialogRef = inject(MatDialogRef);
18678
- // A name of spaces only is refused here, where fsFormRequired lets it by.
18679
- // It reads the control: the name signal catches up only after validation.
18680
- validateName = (control) => {
18681
- const error = viewNameError(control.value);
18682
- if (error) {
18683
- throw new Error(error);
18684
- }
18685
- };
18686
- save = () => {
18687
- return this.data.save(this.name().trim())
18688
- .pipe(tap(() => this._dialogRef.close(true)));
18689
- };
18690
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18691
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportViewDialogComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n {{ data.title }}\n </fs-dialog-title>\n <mat-dialog-content>\n <div class=\"fs-column\">\n <mat-form-field class=\"full-width\">\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n cdkFocusInitial\n [fsFormRequired]=\"true\"\n [fsFormMaxLength]=\"nameMaxLength\"\n [validate]=\"validateName\">\n @if (data.note) {\n <mat-hint>\n {{ data.note }}\n </mat-hint>\n }\n </mat-form-field>\n </div>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions></fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\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.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.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: "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: MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { 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: "ngmodule", type: FsDialogModule }, { kind: "component", type: i3.FsDialogComponent, selector: "fs-dialog", inputs: ["mobileMode", "mobileButtonPlacement", "mobileWidth", "mode", "buttonLayout"] }, { kind: "component", type: i3.FsDialogTitleComponent, selector: "fs-dialog-title", inputs: ["close", "dockToggle", "back"] }, { 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.FsFormMaxLengthDirective, selector: "[fsFormMaxLength]", inputs: ["fsFormMaxLength", "fsFormMaxLengthMessage"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18692
- }
18693
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewDialogComponent, decorators: [{
18694
- type: Component,
18695
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
18696
- FormsModule,
18697
- MatDialogContent,
18698
- MatDialogActions,
18699
- MatFormField,
18700
- MatHint,
18701
- MatLabel,
18702
- MatInput,
18703
- FsDialogModule,
18704
- FsFormModule,
18705
- ], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <fs-dialog-title>\n {{ data.title }}\n </fs-dialog-title>\n <mat-dialog-content>\n <div class=\"fs-column\">\n <mat-form-field class=\"full-width\">\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n cdkFocusInitial\n [fsFormRequired]=\"true\"\n [fsFormMaxLength]=\"nameMaxLength\"\n [validate]=\"validateName\">\n @if (data.note) {\n <mat-hint>\n {{ data.note }}\n </mat-hint>\n }\n </mat-form-field>\n </div>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions></fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>\n" }]
18706
- }] });
18707
-
18708
- // The Views button in the report's header and its menu: the saved views to
18709
- // open and the actions on the open one. What the menu holds is decided in
18710
- // report-view-menu.ts; this draws it and runs the row clicked through
18711
- // ReportViewService, which says 'View saved' and 'View deleted' itself.
18712
- class ReportViewPickerComponent {
18713
- label = computed(() => pickerLabel(this._views.openView(), this._views.viewChanged()));
18714
- tooltip = computed(() => pickerTooltip(this._views.openView(), this._views.viewChanged()) ?? '');
18715
- items = computed(() => viewMenuItems(this._views.openView(), this._views.views(), this._views.viewChanged(), this._views.canShare()));
18716
- // fs-menu's phone sheet has no divider row and would draw an empty one,
18717
- // so the lines show only in the desktop menu. Read when the menu opens.
18718
- dividerShown = () => !this._menu()?.mobile;
18719
- _menu = viewChild(FsMenuComponent);
18720
- _views = inject(ReportViewService);
18721
- _filterState = inject(ReportFilterStateService);
18722
- _dialog = inject(MatDialog);
18723
- _prompt = inject(FsPrompt);
18724
- _destroyRef = inject(DestroyRef);
18725
- run(item) {
18726
- switch (item.kind) {
18727
- case 'view':
18728
- if (item.viewId !== null) {
18729
- this._subscribe(this._views.pick(item.viewId));
18730
- }
18731
- break;
18732
- case 'saveChanges':
18733
- this._subscribe(this._views.saveChanges());
18734
- break;
18735
- case 'saveAsNew':
18736
- this._name(SAVE_AS_TITLE, '', (name) => this._views.saveAsNew({ name }));
18737
- break;
18738
- case 'rename':
18739
- this._name(RENAME_TITLE, this._views.openView()?.name ?? '', (name) => this._views.rename(name));
18740
- break;
18741
- case 'setDefault':
18742
- case 'clearDefault':
18743
- this._subscribe(this._views.setDefault(item.kind === 'setDefault'));
18744
- break;
18745
- case 'share':
18746
- case 'unshare':
18747
- this._subscribe(this._views.share(item.kind === 'share'));
18748
- break;
18749
- case 'delete':
18750
- this._delete();
18751
- break;
18752
- case 'resetToView':
18753
- this._filterState.resetToBase();
18754
- break;
18755
- case 'resetToDefaults':
18756
- this._views.openDefaults();
18757
- break;
18758
- }
18759
- }
18760
- // Opens the name dialog, which saves through `save`.
18761
- _name(title, name, save) {
18762
- this._dialog.open(ReportViewDialogComponent, {
18763
- data: { title, name, note: saveDialogNote(this._hasCrossFilter()), save },
18764
- width: '400px',
18765
- });
18766
- }
18767
- _delete() {
18768
- const open = this._views.openView();
18769
- if (!open) {
18770
- return;
18771
- }
18772
- this._prompt.confirm({
18773
- title: 'Delete view',
18774
- template: deletePromptText(open),
18775
- })
18776
- .pipe(switchMap(() => this._views.deleteOpen()), catchError(() => EMPTY), takeUntilDestroyed(this._destroyRef))
18777
- .subscribe();
18778
- }
18779
- // Runs a call on the views. A failure is shown by the API layer and ends
18780
- // here, so the menu stays usable.
18781
- _subscribe(call) {
18782
- call
18783
- .pipe(catchError(() => EMPTY), takeUntilDestroyed(this._destroyRef))
18784
- .subscribe();
18785
- }
18786
- // Whether a cross-filter is on. The viewer has none yet: the temporary
18787
- // layer gets its cross-filter, and this its reader, with cross-filtering.
18788
- _hasCrossFilter() {
18789
- return false;
18790
- }
18791
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewPickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18792
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ReportViewPickerComponent, isStandalone: true, selector: "app-report-view-picker", viewQueries: [{ propertyName: "_menu", first: true, predicate: FsMenuComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<button\n mat-button\n type=\"button\"\n [matTooltip]=\"tooltip()\"\n [fsMenuTriggerFor]=\"viewsMenu\">\n <span class=\"report-view-picker__label\">{{ label() }}</span>\n <mat-icon iconPositionEnd>arrow_drop_down</mat-icon>\n</button>\n<fs-menu #viewsMenu>\n @for (item of items(); track item.kind + ':' + item.viewId) {\n @if (item.divider) {\n <ng-template\n fs-menu-divider-item\n [show]=\"dividerShown\">\n </ng-template>\n }\n <ng-template\n fs-menu-item\n [disabled]=\"item.disabled\"\n [tooltip]=\"item.tooltip ?? ''\"\n (click)=\"run(item)\">\n @if (item.open) {\n <mat-icon>check</mat-icon>\n }\n @if (item.icon) {\n <mat-icon>{{ item.icon }}</mat-icon>\n }\n {{ item.label }}\n </ng-template>\n }\n</fs-menu>\n", styles: [":host{flex:0 0 auto}.report-view-picker__label{display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}\n"], dependencies: [{ kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "directive", type: i2.FsMenuDividerItemDirective, selector: "[fs-menu-divider-item]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18793
- }
18794
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportViewPickerComponent, decorators: [{
18795
- type: Component,
18796
- args: [{ selector: 'app-report-view-picker', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [
18797
- FsMenuModule,
18798
- MatButtonModule,
18799
- MatIconModule,
18800
- MatTooltipModule,
18801
- ], template: "<button\n mat-button\n type=\"button\"\n [matTooltip]=\"tooltip()\"\n [fsMenuTriggerFor]=\"viewsMenu\">\n <span class=\"report-view-picker__label\">{{ label() }}</span>\n <mat-icon iconPositionEnd>arrow_drop_down</mat-icon>\n</button>\n<fs-menu #viewsMenu>\n @for (item of items(); track item.kind + ':' + item.viewId) {\n @if (item.divider) {\n <ng-template\n fs-menu-divider-item\n [show]=\"dividerShown\">\n </ng-template>\n }\n <ng-template\n fs-menu-item\n [disabled]=\"item.disabled\"\n [tooltip]=\"item.tooltip ?? ''\"\n (click)=\"run(item)\">\n @if (item.open) {\n <mat-icon>check</mat-icon>\n }\n @if (item.icon) {\n <mat-icon>{{ item.icon }}</mat-icon>\n }\n {{ item.label }}\n </ng-template>\n }\n</fs-menu>\n", styles: [":host{flex:0 0 auto}.report-view-picker__label{display:inline-block;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}\n"] }]
18802
- }] });
18803
-
18804
17654
  // The report view's chat gateway: the stock one, plus fields read at the
18805
17655
  // moment each turn is sent and merged into that turn's body only. The chat's
18806
17656
  // requestData can't carry them, because it is also the key of the conversation
@@ -18824,7 +17674,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
18824
17674
  }] });
18825
17675
 
18826
17676
  // The parts of the chat's viewer state (ReportChatViewerState) read off a
18827
- // view document of the screen (the state service's snapshot()). Pure, so the
17677
+ // snapshot of the screen (the state service's snapshot()). Pure, so the
18828
17678
  // node harness checks them. The caps are the server's: it reads at most
18829
17679
  // CHAT_DRILLS_MAX drilled charts, DRILL_MAX_DEPTH steps a path and
18830
17680
  // CHAT_LABEL_MAX characters a label, and drops the rest.
@@ -18855,6 +17705,96 @@ function chatViewerDrills(snapshot, locale) {
18855
17705
  }));
18856
17706
  }
18857
17707
 
17708
+ // The groups a saved filter holds: the report-wide ones.
17709
+ function savedFilterGroups(groups) {
17710
+ return (groups ?? []).filter((group) => group.level === 'report' || group.level === 'both');
17711
+ }
17712
+ // A saved filter's values kept to `groups` (savedFilterGroups): each
17713
+ // report-wide group's item value, in a shape its item takes, a date range's
17714
+ // days as yyyy-MM-dd (a day is the same day in every zone, so everyone who
17715
+ // picks a shared filter gets the same days). Any other key is left out, and
17716
+ // so is a value that holds nothing or does not fit its group's type.
17717
+ function savedFilterValues(filters, groups) {
17718
+ const kept = {};
17719
+ groups.forEach((group) => {
17720
+ const name = groupItemName(group);
17721
+ const value = filters && Object.prototype.hasOwnProperty.call(filters, name)
17722
+ ? itemValue(group, filters[name])
17723
+ : null;
17724
+ if (value !== null) {
17725
+ kept[name] = value;
17726
+ }
17727
+ });
17728
+ return kept;
17729
+ }
17730
+ // One item value as a saved filter keeps it, or null.
17731
+ function itemValue(group, value) {
17732
+ switch (group.type) {
17733
+ case 'dateRange':
17734
+ return dateRangeValue(value);
17735
+ case 'select':
17736
+ return selectValue(value);
17737
+ case 'numberRange':
17738
+ // A number range shows as a keyword box where the server does not
17739
+ // serve numberRange, and its item then holds text.
17740
+ return isPlainObject(value) ? rangeValue(value) : textValue(value);
17741
+ default:
17742
+ return textValue(value);
17743
+ }
17744
+ }
17745
+ // A date range item's { from, to }, each end a calendar day.
17746
+ function dateRangeValue(value) {
17747
+ if (!isPlainObject(value)) {
17748
+ return null;
17749
+ }
17750
+ const from = dayText(value.from);
17751
+ const to = dayText(value.to);
17752
+ if (!from && !to) {
17753
+ return null;
17754
+ }
17755
+ return { ...(from ? { from } : {}), ...(to ? { to } : {}) };
17756
+ }
17757
+ // A select item's picks ({ name, value } pairs), or its left-out picks
17758
+ // ({ exclude: true, selected }).
17759
+ function selectValue(value) {
17760
+ if (isPlainObject(value)) {
17761
+ const selected = picks(value.selected);
17762
+ return selected ? { exclude: value.exclude === true, selected } : null;
17763
+ }
17764
+ return picks(value);
17765
+ }
17766
+ // A range item's { min, max }; either end may be open, and 0 is an end.
17767
+ function rangeValue(value) {
17768
+ const min = finiteNumber(value.min);
17769
+ const max = finiteNumber(value.max);
17770
+ if (min === undefined && max === undefined) {
17771
+ return null;
17772
+ }
17773
+ return { ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}) };
17774
+ }
17775
+ function textValue(value) {
17776
+ const text = typeof value === 'string' || typeof value === 'number' ? String(value).trim() : '';
17777
+ return text || null;
17778
+ }
17779
+ // The picks of a select item that name a value, or null when none does.
17780
+ function picks(value) {
17781
+ const kept = (Array.isArray(value) ? value : [])
17782
+ .filter((pick) => isPlainObject(pick)
17783
+ && pick.value !== null && pick.value !== undefined && pick.value !== '');
17784
+ return kept.length ? kept : null;
17785
+ }
17786
+ // A day as yyyy-MM-dd: a Date's local day, or the first ten characters of a
17787
+ // day already written as text. Null for anything else.
17788
+ function dayText(value) {
17789
+ if (value instanceof Date) {
17790
+ return Number.isNaN(value.getTime()) ? null : dateBoundString(value) ?? null;
17791
+ }
17792
+ return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : null;
17793
+ }
17794
+ function isPlainObject(value) {
17795
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date);
17796
+ }
17797
+
18858
17798
  // A single report: the AI chat on the left, the report canvas on the right,
18859
17799
  // under an H2 title + settings gear. The host supplies [reportId] (from its
18860
17800
  // route) and switches reports through its own routes — this component never
@@ -18868,9 +17808,8 @@ class FsAiReportComponent {
18868
17808
  // can name a page: ?page=7). It is read once, when a report opens, and
18869
17809
  // never written: a report keeps no state in the browser URL. Its filters,
18870
17810
  // the page on screen and the rest live in the report's state
18871
- // (ReportFilterStateService), so a reload opens the report on its defaults,
18872
- // or on the viewer's default view. A host that wants no page read sets this
18873
- // to null.
17811
+ // (ReportFilterStateService), so a reload opens the report on its
17812
+ // defaults. A host that wants no page read sets this to null.
18874
17813
  pageQueryParam = input('page');
18875
17814
  // The report was deleted from its settings dialog — the host routes back to
18876
17815
  // the listing.
@@ -18884,9 +17823,8 @@ class FsAiReportComponent {
18884
17823
  report = signal(null);
18885
17824
  editMode = signal(false);
18886
17825
  // The page the canvas should open on: the ?page= the report opened with
18887
- // (read once per report), or an opened view's page. Cleared as soon as the
18888
- // canvas reports back, so a later in-app page change is never fought by a
18889
- // stale value.
17826
+ // (read once per report). Cleared as soon as the canvas reports back, so a
17827
+ // later in-app page change is never fought by a stale value.
18890
17828
  pageId = signal(null);
18891
17829
  // The page the chat is bound to: the one on screen. A report conversation
18892
17830
  // belongs to exactly one page, so this both scopes the conversation list and
@@ -18928,8 +17866,8 @@ class FsAiReportComponent {
18928
17866
  // rebuilt config) whenever the report-level filter set changes.
18929
17867
  reportFilterKey = computed(() => {
18930
17868
  return [
18931
- // The state version the bar was built for: an applied view, or a reset
18932
- // to the base, replaces every value at once.
17869
+ // The state version the bar was built for: a reset to the defaults
17870
+ // replaces every value at once.
18933
17871
  this._barVersion(),
18934
17872
  // The effective zone is part of it too: a zone change re-resolves relative
18935
17873
  // presets, and the bar must be recreated to show the new days.
@@ -18944,31 +17882,36 @@ class FsAiReportComponent {
18944
17882
  ...(hasPageGroup(this.report()) ? [`page:${this.chatPageId()}`] : []),
18945
17883
  ].join('|');
18946
17884
  });
18947
- // The server keeps saved views for this report, so the Views picker shows.
18948
- viewsServed = computed(() => this._views.serves(this.report()));
18949
17885
  _split;
18950
17886
  _chatPanel;
18951
17887
  // Only the canvas knows which page is on screen, and a page-scoped export
18952
17888
  // has to ask it.
18953
17889
  _canvas;
17890
+ // The report-level filter bar, while it is drawn. A saved filter being
17891
+ // picked reads the bar's values off it (_keepBarOnPick).
17892
+ _reportBar;
18954
17893
  _reportData = inject(ReportData);
18955
17894
  _reportAiChatData = inject(ReportAiChatData);
18956
17895
  _route = inject(ActivatedRoute);
18957
17896
  _filterState = inject(ReportFilterStateService);
18958
17897
  _filterOptions = inject(ReportFilterOptionsService);
18959
- _views = inject(ReportViewService);
18960
17898
  _exportService = inject(ReportExportService);
18961
17899
  _collector = inject(ReportExportCollectorService);
18962
17900
  _process = inject(FsProcess);
18963
17901
  _dialog = inject(MatDialog);
18964
17902
  _destroyRef = inject(DestroyRef);
18965
17903
  _zone = inject(NgZone);
18966
- // The report that has been opened (its default view, else its defaults).
18967
- // A report opens once; a reload after a chat or dialog edit keeps the state
18968
- // in memory.
18969
- _openedReportId = null;
18970
17904
  // The state version the bar was last built for (reportFilterKey).
18971
17905
  _barVersion = signal(0);
17906
+ // The report bar's values as they were the moment fs-filter read a saved
17907
+ // filter's values, or null outside that moment. A pick sets every item of
17908
+ // the bar from the saved filter's values; each item a saved filter never
17909
+ // holds (the group-by grain, the match, a page's own filters) is put back
17910
+ // to its noted value as it is set (_keepItemOnPick).
17911
+ _barValuesAtPick = null;
17912
+ // The report the screen was opened on its first saved filter for
17913
+ // (_openOnFirstSavedFilter), or null before that.
17914
+ _openedOnSavedFilterReportId = null;
18972
17915
  constructor() {
18973
17916
  // Each chat turn carries what the viewer has on screen. Only the function
18974
17917
  // is set here; it is called when a turn is sent.
@@ -18997,17 +17940,14 @@ class FsAiReportComponent {
18997
17940
  this._loadReport(reportId);
18998
17941
  }
18999
17942
  else {
19000
- // Forget the opened report, so a host that comes back to it opens
19001
- // it again.
19002
- this._openedReportId = null;
19003
17943
  this.report.set(null);
19004
17944
  this._buildReportConfig();
19005
17945
  }
19006
17946
  });
19007
17947
  });
19008
- // apply() and resetToBase() move the state version. The bar is built
19009
- // again for it, and reportFilterKey, which carries it, recreates
19010
- // fs-filter: it reads its config once.
17948
+ // resetToDefaults() moves the state version. The bar is built again for
17949
+ // it, and reportFilterKey, which carries it, recreates fs-filter: it reads
17950
+ // its config once.
19011
17951
  effect(() => {
19012
17952
  const version = this._filterState.stateVersion();
19013
17953
  untracked(() => {
@@ -19016,12 +17956,6 @@ class FsAiReportComponent {
19016
17956
  }
19017
17957
  });
19018
17958
  });
19019
- // An opened view (a saved view picked, or the report's defaults) opens
19020
- // on its page.
19021
- effect(() => {
19022
- this._filterState.appliedVersion();
19023
- untracked(() => this._openAppliedPage());
19024
- });
19025
17959
  this._buildReportConfig();
19026
17960
  }
19027
17961
  toggleEditMode() {
@@ -19117,7 +18051,7 @@ class FsAiReportComponent {
19117
18051
  // The asked-for page has been consumed; from here the canvas owns the page.
19118
18052
  this.pageId.set(null);
19119
18053
  this.chatPageId.set(pageId);
19120
- // The open page is part of the screen a view saves.
18054
+ // The open page is part of the state the tiles and exports read.
19121
18055
  this._filterState.setPage(pageId);
19122
18056
  this.pageChanged.emit(pageId);
19123
18057
  // The bar shows the page groups of the page on screen.
@@ -19158,12 +18092,12 @@ class FsAiReportComponent {
19158
18092
  // search and hidden columns, a chart's drill path and the groups that match
19159
18093
  // 'any'; a server that does not read it uses each list's saved sort. The
19160
18094
  // notes get the filter lines as the cover words them (exportLines; a This
19161
- // Page zip names only the groups that reach the page) and the open view,
19162
- // which the server names, and each file is named after its tile's title as
19163
- // the canvas header reads it (titles). All of it is read once, when the
19164
- // export starts (exportWording). `thisPage` narrows it to the page the
19165
- // canvas is showing. Unlike the PDF/PowerPoint exports the files are
19166
- // written server-side, so they are not capped by what a slide can hold.
18095
+ // Page zip names only the groups that reach the page), and each file is
18096
+ // named after its tile's title as the canvas header reads it (titles). All
18097
+ // of it is read once, when the export starts (exportWording). `thisPage`
18098
+ // narrows it to the page the canvas is showing. Unlike the PDF/PowerPoint
18099
+ // exports the files are written server-side, so they are not capped by
18100
+ // what a slide can hold.
19167
18101
  exportData(thisPage = false) {
19168
18102
  const report = this.report();
19169
18103
  if (!report) {
@@ -19173,11 +18107,9 @@ class FsAiReportComponent {
19173
18107
  const filters = this._filterState.resolveForReport(report, pageId);
19174
18108
  const frequency = this._filterState.frequency();
19175
18109
  const snapshot = this._collector.snapshot(report);
19176
- const states = zipExportStates(report, snapshot.view, pageId);
19177
- const viewId = this._views.openView()?.id ?? null;
19178
- const viewChanged = this._views.viewChanged();
18110
+ const states = zipExportStates(report, snapshot.state, pageId);
19179
18111
  this._process.download('Exporting data', this._collector.exportWording(report, snapshot, pageId)
19180
- .pipe(switchMap(({ lines, titles }) => this._reportData.exportReport(report.id, filters, pageId, frequency, lines, states, viewId, viewChanged, titles))));
18112
+ .pipe(switchMap(({ lines, titles }) => this._reportData.exportReport(report.id, filters, pageId, frequency, lines, states, titles))));
19181
18113
  }
19182
18114
  // React to each chat turn (fs-chat handles the request + persistence); we
19183
18115
  // only care about the report side effects this context's backend flags.
@@ -19218,8 +18150,7 @@ class FsAiReportComponent {
19218
18150
  // What the viewer has on screen as a chat turn sends it. The canvas
19219
18151
  // selection counts only in edit mode: it is drawn only then, and it is kept
19220
18152
  // (not cleared) when edit mode is left. The match is sent only while it is
19221
- // 'any', the drilled charts only when one is drilled, and the open view
19222
- // (and whether the screen differs from it) only while one is open.
18153
+ // 'any', and the drilled charts only when one is drilled.
19223
18154
  _chatViewerState() {
19224
18155
  const snapshot = this._filterState.snapshot();
19225
18156
  const drills = chatViewerDrills(snapshot, this._filterState.formatContext().locale);
@@ -19229,7 +18160,6 @@ class FsAiReportComponent {
19229
18160
  filters: this._filterState.viewerFilters(),
19230
18161
  ...chatViewerMatch(snapshot),
19231
18162
  ...(drills.length ? { drills } : {}),
19232
- ...chatViewFields(this._views.openView(), this._views.viewChanged()),
19233
18163
  };
19234
18164
  }
19235
18165
  _resizeTo(clientX) {
@@ -19251,45 +18181,20 @@ class FsAiReportComponent {
19251
18181
  const pageId = Number(this._route.snapshot.queryParamMap.get(param));
19252
18182
  return Number.isFinite(pageId) && pageId > 0 ? pageId : null;
19253
18183
  }
18184
+ // Every load sets the viewer state up for the report (init): its first
18185
+ // load opens it on its authored defaults, and a later one (a chat edit, a
18186
+ // settings save) keeps the state in memory.
19254
18187
  _loadReport(reportId, emitRenamed = false) {
19255
18188
  this._reportData.get(reportId)
19256
- .pipe(switchMap((report) => this._openReport(report).pipe(map(() => report))), takeUntilDestroyed(this._destroyRef))
18189
+ .pipe(tap((report) => this._filterState.init(report)), takeUntilDestroyed(this._destroyRef))
19257
18190
  .subscribe((report) => {
19258
18191
  this.report.set(report);
19259
- this._openAppliedPage();
19260
18192
  this._buildReportConfig();
19261
18193
  if (emitRenamed) {
19262
18194
  this.renamed.emit(report);
19263
18195
  }
19264
18196
  });
19265
18197
  }
19266
- // Sets the viewer state up for a loaded report. On its first load a server
19267
- // that keeps saved views opens it on the viewer's default view, else on the
19268
- // defaults (which waits for the list of views); any other server opens it
19269
- // on the defaults. Every later load keeps the state in memory.
19270
- _openReport(report) {
19271
- const firstLoad = report.id !== this._openedReportId;
19272
- this._openedReportId = report.id;
19273
- if (!this._views.serves(report)) {
19274
- this._filterState.init(report);
19275
- return of(undefined);
19276
- }
19277
- if (!firstLoad) {
19278
- this._filterState.init(report);
19279
- this._views.refresh(report);
19280
- return of(undefined);
19281
- }
19282
- return this._views.open(report, this.pageId());
19283
- }
19284
- // Moves the canvas to the page an opened view put on screen (a ?page= the
19285
- // report opened with already won inside apply()). A reset, or a reload,
19286
- // leaves the page.
19287
- _openAppliedPage() {
19288
- const pageId = this._filterState.page();
19289
- if (pageId && pageId !== this.chatPageId()) {
19290
- this.pageId.set(pageId);
19291
- }
19292
- }
19293
18198
  // Report-level filter groups in their saved order — the ones that render in
19294
18199
  // the toolbar (component-level groups render on their components), with
19295
18200
  // the page groups of the page on screen.
@@ -19323,26 +18228,134 @@ class FsAiReportComponent {
19323
18228
  if (matchItem) {
19324
18229
  items.push(matchItem);
19325
18230
  }
18231
+ const savedFilters = this._savedFiltersConfig();
19326
18232
  this._barVersion.set(this._filterState.stateVersion());
19327
18233
  this.reportFilterConfig.set({
19328
18234
  // fs-filter never touches the URL or persists filter state: a report
19329
18235
  // keeps no state in the URL. ReportFilterStateService owns the values.
19330
18236
  queryParam: false,
19331
18237
  persist: false,
19332
- items,
18238
+ items: savedFilters ? this._keepOnPick(items) : items,
19333
18239
  change: (query) => this._barChanged(query ?? {}, !!matchItem),
19334
- // 'Clear filters' puts the screen back on the open view, else on the
19335
- // report's defaults, and drops the temporary layer. fs-filter reports
19336
- // its own cleared items (change) just before this, so the reset wins.
19337
- // A server without saved views keeps the clear it always had: the
19338
- // bar's items only.
19339
- clear: this._views.serves(report) ? () => this._filterState.resetToBase() : undefined,
18240
+ // 'Clear filters' puts the screen back on the report's authored
18241
+ // defaults and drops the temporary layer. fs-filter reports its own
18242
+ // cleared items (change) just before this, so the reset wins.
18243
+ clear: () => this._filterState.resetToDefaults(),
19340
18244
  // The chip shows exactly while the screen differs from what a clear
19341
- // returns to (C5): right after a save it hides, and once the open view
19342
- // is deleted it shows for what differs from the defaults.
19343
- clearVisible: this._views.serves(report) ? () => this._filterState.viewChanged() : undefined,
18245
+ // returns to (C5).
18246
+ clearVisible: () => this._filterState.differsFromDefaults(),
18247
+ savedFilters,
18248
+ // fs-filter calls init once its saved filters are loaded.
18249
+ ...(savedFilters && reportId ? { init: () => this._openOnFirstSavedFilter(reportId) } : {}),
19344
18250
  });
19345
18251
  }
18252
+ // A report opens on its first saved filter: once its saved filters are
18253
+ // first loaded, the first one is picked as a viewer would pick it, so it
18254
+ // applies on every page through the same change (_barChanged). Once per
18255
+ // report opened: the bar is recreated as the screen changes, and a later
18256
+ // build must not pick it again over the viewer's own edits. Reordering the
18257
+ // saved filters changes which one a report opens on, for everyone.
18258
+ _openOnFirstSavedFilter(reportId) {
18259
+ const savedFilterController = this._reportBar?.savedFilterController;
18260
+ if (this._openedOnSavedFilterReportId === reportId || !savedFilterController) {
18261
+ return;
18262
+ }
18263
+ this._openedOnSavedFilterReportId = reportId;
18264
+ const [first] = savedFilterController.savedFilters;
18265
+ if (first) {
18266
+ savedFilterController.setActiveFilter(first);
18267
+ }
18268
+ }
18269
+ // The bar's saved filters, shared by everyone who may open the report, or
18270
+ // undefined when the bar has none to offer: a report not yet saved has no
18271
+ // id to keep them under, and a bar with no report-wide filter has nothing
18272
+ // a saved filter holds. fs-filter's own Saved filters menu lists them,
18273
+ // saves the bar's values as a new one or over the picked one, orders and
18274
+ // deletes them. What it saves is kept to the report-wide filters
18275
+ // (savedFilterValues): a page's own filters, the grain and the match are
18276
+ // left out. The list is read every time the bar is built; one that cannot
18277
+ // be read shows as empty rather than keeping the bar from drawing.
18278
+ _savedFiltersConfig() {
18279
+ const reportId = this.report()?.id ?? null;
18280
+ if (!reportId || !savedFilterGroups(this._reportGroups()).length) {
18281
+ return undefined;
18282
+ }
18283
+ return {
18284
+ load: () => this._reportData
18285
+ .savedFilters(reportId, { context: new HttpContext().set(DisplayApiError, false) })
18286
+ .pipe(catchError(() => of([])), map((savedFilters) => (savedFilters ?? []).map((savedFilter) => this._barSavedFilter(savedFilter)))),
18287
+ save: (savedFilter) => {
18288
+ const filters = savedFilterValues(savedFilter.filters, this._savedFilterGroups());
18289
+ const save$ = savedFilter.id
18290
+ ? this._reportData.updateSavedFilter(reportId, Number(savedFilter.id), {
18291
+ ...(savedFilter.name ? { name: savedFilter.name } : {}),
18292
+ filters,
18293
+ })
18294
+ : this._reportData.createSavedFilter(reportId, { name: savedFilter.name ?? '', filters });
18295
+ return save$.pipe(map((saved) => this._barSavedFilter(saved)));
18296
+ },
18297
+ order: (savedFilters) => this._reportData
18298
+ .saveSavedFilterPositions(reportId, savedFilters.map((savedFilter) => Number(savedFilter.id)))
18299
+ .pipe(map((ordered) => (ordered ?? []).map((savedFilter) => this._barSavedFilter(savedFilter)))),
18300
+ delete: (savedFilter) => this._reportData
18301
+ .deleteSavedFilter(reportId, Number(savedFilter.id))
18302
+ .pipe(map(() => savedFilter)),
18303
+ };
18304
+ }
18305
+ // The report's report-wide filter groups: the ones a saved filter holds.
18306
+ _savedFilterGroups() {
18307
+ return savedFilterGroups(this.report()?.filterGroups);
18308
+ }
18309
+ // A saved filter as the bar's menu holds it. Its values are kept to the
18310
+ // report-wide filters the report has now (a key it no longer has, or a
18311
+ // value its filter can no longer take, is dropped rather than applied), and
18312
+ // each read of them hands out a copy, so nothing the bar does to the values
18313
+ // it set changes the saved filter. A read also notes the bar's values
18314
+ // (_keepBarOnPick): picking a saved filter reads its values and sets the
18315
+ // bar from them in one go, and the items it does not hold are put back.
18316
+ _barSavedFilter(savedFilter) {
18317
+ const filters = savedFilterValues(savedFilter.filters, this._savedFilterGroups());
18318
+ const keepBar = () => this._keepBarOnPick();
18319
+ return {
18320
+ id: savedFilter.id,
18321
+ name: savedFilter.name,
18322
+ get filters() {
18323
+ keepBar();
18324
+ return JSON.parse(JSON.stringify(filters));
18325
+ },
18326
+ };
18327
+ }
18328
+ // Notes the bar's values for a pick of a saved filter (_barValuesAtPick).
18329
+ // fs-filter sets the bar from the saved filter's values in the same task
18330
+ // as it reads them, so the note is dropped once this task is done: any
18331
+ // other read of a saved filter's values (its row in View all, a save) sets
18332
+ // nothing.
18333
+ _keepBarOnPick() {
18334
+ if (!this._reportBar) {
18335
+ return;
18336
+ }
18337
+ this._barValuesAtPick = { ...this._reportBar.values };
18338
+ queueMicrotask(() => this._barValuesAtPick = null);
18339
+ }
18340
+ // The bar's items, each one a saved filter does not hold (the group-by
18341
+ // grain, the match, a page's own filters) set back to its value when a pick
18342
+ // of a saved filter sets it. fs-filter sets every item from a saved filter
18343
+ // and calls each one's change before it reports the bar's change, so the
18344
+ // change the bar reports carries these items as they were: the pick moves
18345
+ // the report-wide filters only, on every page, through the same change as
18346
+ // a viewer's own edit (_barChanged). On any other change an item's change
18347
+ // comes after the bar's, with no pick noted, and does nothing.
18348
+ _keepOnPick(items) {
18349
+ const held = new Set(this._savedFilterGroups().map((group) => groupItemName(group)));
18350
+ return items.map((item) => (held.has(item.name)
18351
+ ? item
18352
+ : { ...item, change: (barItem) => this._keepItemOnPick(barItem) }));
18353
+ }
18354
+ _keepItemOnPick(item) {
18355
+ if (this._barValuesAtPick) {
18356
+ item.setValue(this._barValuesAtPick[item.name], false);
18357
+ }
18358
+ }
19346
18359
  // The bar's values into the state: every group it shows, the grain and,
19347
18360
  // when the bar has the control, the match.
19348
18361
  _barChanged(query, hasMatch) {
@@ -19377,8 +18390,6 @@ class FsAiReportComponent {
19377
18390
  ReportFilterStateService,
19378
18391
  // The bars' select lists, narrowed by the other filters; per open report.
19379
18392
  ReportFilterOptionsService,
19380
- // The report's saved views and the order it opens in; per open report.
19381
- ReportViewService,
19382
18393
  ReportExportCollectorService,
19383
18394
  ReportPptxService,
19384
18395
  ReportPdfService,
@@ -19389,11 +18400,11 @@ class FsAiReportComponent {
19389
18400
  ReportAiChatData,
19390
18401
  { provide: FsAiChatData, useExisting: ReportAiChatData },
19391
18402
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
19392
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CyHkFToO.mjs').then((module) => module.default) }),
18403
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DKXlgu1D.mjs').then((module) => module.default) }),
19393
18404
  // The bar's date pickers start their weeks on Monday and list the built-in
19394
18405
  // presets; a preset only sets the days.
19395
18406
  provideReportDatePicker(),
19396
- ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report()?.name }}</h2>\n\n <!-- The saved views: shown only when the server keeps them. -->\n @if (viewsServed()) {\n <app-report-view-picker></app-report-view-picker>\n }\n\n @if (report()) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }, { kind: "component", type: ReportViewPickerComponent, selector: "app-report-view-picker" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18407
+ ], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }, { propertyName: "_reportBar", first: true, predicate: FilterComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report()?.name }}</h2>\n\n @if (report()) {\n <!-- Report-level filters, inline with the toolbar actions, with the\n report's saved filters in fs-filter's own Saved filters menu.\n fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the\n block is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19397
18408
  }
19398
18409
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
19399
18410
  type: Component,
@@ -19403,8 +18414,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19403
18414
  ReportFilterStateService,
19404
18415
  // The bars' select lists, narrowed by the other filters; per open report.
19405
18416
  ReportFilterOptionsService,
19406
- // The report's saved views and the order it opens in; per open report.
19407
- ReportViewService,
19408
18417
  ReportExportCollectorService,
19409
18418
  ReportPptxService,
19410
18419
  ReportPdfService,
@@ -19415,7 +18424,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19415
18424
  ReportAiChatData,
19416
18425
  { provide: FsAiChatData, useExisting: ReportAiChatData },
19417
18426
  // Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
19418
- provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CyHkFToO.mjs').then((module) => module.default) }),
18427
+ provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-DKXlgu1D.mjs').then((module) => module.default) }),
19419
18428
  // The bar's date pickers start their weeks on Monday and list the built-in
19420
18429
  // presets; a preset only sets the days.
19421
18430
  provideReportDatePicker(),
@@ -19427,8 +18436,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19427
18436
  MatIconModule,
19428
18437
  MatTooltipModule,
19429
18438
  ReportCanvasComponent,
19430
- ReportViewPickerComponent,
19431
- ], template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report()?.name }}</h2>\n\n <!-- The saved views: shown only when the server keeps them. -->\n @if (viewsServed()) {\n <app-report-view-picker></app-report-view-picker>\n }\n\n @if (report()) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
18439
+ ], template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report()?.name }}</h2>\n\n @if (report()) {\n <!-- Report-level filters, inline with the toolbar actions, with the\n report's saved filters in fs-filter's own Saved filters menu.\n fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the\n block is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"] }]
19432
18440
  }], ctorParameters: () => [], propDecorators: { _split: [{
19433
18441
  type: ViewChild,
19434
18442
  args: ['split', { static: true }]
@@ -19438,6 +18446,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19438
18446
  }], _canvas: [{
19439
18447
  type: ViewChild,
19440
18448
  args: [ReportCanvasComponent]
18449
+ }], _reportBar: [{
18450
+ type: ViewChild,
18451
+ args: [FilterComponent]
19441
18452
  }] } });
19442
18453
 
19443
18454
  /*
@@ -19453,4 +18464,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19453
18464
  */
19454
18465
 
19455
18466
  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 };
19456
- //# sourceMappingURL=firestitch-report-firestitch-report-DftB7M1o.mjs.map
18467
+ //# sourceMappingURL=firestitch-report-firestitch-report-Bnv62suw.mjs.map