@cdc/dashboard 4.26.3 → 4.26.5

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 (151) hide show
  1. package/CONFIG.md +219 -0
  2. package/README.md +60 -20
  3. package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcdashboard.js +61559 -58048
  6. package/examples/__data__/data-2.json +6 -0
  7. package/examples/__data__/data.json +6 -0
  8. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  9. package/examples/dashboard-missing-datasets-multi.json +174 -0
  10. package/examples/dashboard-missing-datasets-single.json +121 -0
  11. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  12. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  13. package/examples/dashboard-stale-dataset-keys.json +181 -0
  14. package/examples/dashboard-tiered-filter-regression.json +190 -0
  15. package/examples/legend-issue.json +1 -1
  16. package/examples/minimal-example.json +34 -0
  17. package/examples/private/cfa-dashboard.json +651 -0
  18. package/examples/private/data-bite-wrap.json +6936 -0
  19. package/examples/private/dengue.json +4640 -0
  20. package/examples/private/link_to_file.json +16662 -0
  21. package/examples/private/multi-dash-fix.json +16963 -0
  22. package/examples/private/versions.json +41612 -0
  23. package/examples/sankey.json +3 -3
  24. package/examples/test-api-filter-reset.json +4 -4
  25. package/examples/tp5-test.json +86 -4
  26. package/examples/us-map-filter-example.json +1074 -0
  27. package/package.json +9 -9
  28. package/src/CdcDashboard.tsx +6 -2
  29. package/src/CdcDashboardComponent.tsx +179 -88
  30. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  31. package/src/DashboardCopyPasteContext.tsx +48 -0
  32. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  33. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  34. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  35. package/src/_stories/Dashboard.smoke.stories.tsx +33 -0
  36. package/src/_stories/Dashboard.stories.tsx +337 -2
  37. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  38. package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
  39. package/src/_stories/_mock/tp5-test.json +86 -5
  40. package/src/components/Column.test.tsx +176 -0
  41. package/src/components/Column.tsx +214 -13
  42. package/src/components/DashboardConditionModal.test.tsx +420 -0
  43. package/src/components/DashboardConditionModal.tsx +367 -0
  44. package/src/components/DashboardConditionSummary.tsx +59 -0
  45. package/src/components/DashboardEditors.tsx +23 -0
  46. package/src/components/DashboardFilters/DashboardFilters.test.tsx +267 -0
  47. package/src/components/DashboardFilters/DashboardFilters.tsx +193 -172
  48. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  49. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +46 -6
  50. package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
  51. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
  52. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +304 -0
  53. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +43 -36
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
  55. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  56. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  57. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  58. package/src/components/DataDesignerModal.tsx +2 -1
  59. package/src/components/ExpandCollapseButtons.tsx +6 -4
  60. package/src/components/Grid.tsx +12 -7
  61. package/src/components/Header/Header.tsx +36 -17
  62. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
  63. package/src/components/Row.test.tsx +228 -0
  64. package/src/components/Row.tsx +104 -28
  65. package/src/components/VisualizationRow.test.tsx +396 -0
  66. package/src/components/VisualizationRow.tsx +177 -51
  67. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  68. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  69. package/src/components/Widget/Widget.test.tsx +218 -0
  70. package/src/components/Widget/Widget.tsx +123 -20
  71. package/src/components/Widget/widget.styles.css +58 -14
  72. package/src/components/dashboard-condition-modal.css +76 -0
  73. package/src/components/dashboard-condition-summary.css +87 -0
  74. package/src/data/initial-state.js +1 -0
  75. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  76. package/src/helpers/addVisualization.ts +17 -4
  77. package/src/helpers/cloneDashboardWidget.ts +127 -0
  78. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  79. package/src/helpers/dashboardConditionUi.ts +47 -0
  80. package/src/helpers/dashboardConditions.ts +200 -0
  81. package/src/helpers/dashboardFilterTargets.ts +156 -0
  82. package/src/helpers/filterData.ts +4 -9
  83. package/src/helpers/filterVisibility.ts +20 -0
  84. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  85. package/src/helpers/getFilteredData.ts +18 -5
  86. package/src/helpers/getUpdateConfig.ts +43 -12
  87. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  88. package/src/helpers/iconHash.tsx +9 -3
  89. package/src/helpers/mapDataToConfig.ts +31 -29
  90. package/src/helpers/reloadURLHelpers.ts +25 -5
  91. package/src/helpers/removeDashboardFilter.ts +33 -33
  92. package/src/helpers/tests/addVisualization.test.ts +53 -9
  93. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  94. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  95. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  96. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  97. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  98. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  99. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  100. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  101. package/src/index.tsx +6 -3
  102. package/src/scss/grid.scss +281 -22
  103. package/src/scss/main.scss +215 -64
  104. package/src/store/dashboard.actions.ts +17 -4
  105. package/src/store/dashboard.reducer.test.ts +538 -0
  106. package/src/store/dashboard.reducer.ts +136 -22
  107. package/src/test/CdcDashboard.test.jsx +24 -0
  108. package/src/test/CdcDashboard.test.tsx +148 -0
  109. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  110. package/src/types/ConfigRow.ts +15 -0
  111. package/src/types/DashboardFilters.ts +4 -0
  112. package/src/types/SharedFilter.ts +2 -0
  113. package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
  114. package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
  115. package/examples/DEV-6574.json +0 -2224
  116. package/examples/api-dashboard-data.json +0 -272
  117. package/examples/api-dashboard-years.json +0 -11
  118. package/examples/api-geographies-data.json +0 -11
  119. package/examples/chart-data.json +0 -5409
  120. package/examples/custom/css/respiratory.css +0 -236
  121. package/examples/custom/js/respiratory.js +0 -242
  122. package/examples/default-data.json +0 -368
  123. package/examples/default-filter-control.json +0 -209
  124. package/examples/default-multi-dataset-shared-filter.json +0 -1729
  125. package/examples/default-multi-dataset.json +0 -506
  126. package/examples/ed-visits-county-file.json +0 -402
  127. package/examples/filters/Alabama.json +0 -72
  128. package/examples/filters/Alaska.json +0 -1737
  129. package/examples/filters/Arkansas.json +0 -4713
  130. package/examples/filters/California.json +0 -212
  131. package/examples/filters/Colorado.json +0 -1500
  132. package/examples/filters/Connecticut.json +0 -559
  133. package/examples/filters/Delaware.json +0 -63
  134. package/examples/filters/DistrictofColumbia.json +0 -63
  135. package/examples/filters/Florida.json +0 -4217
  136. package/examples/filters/States.json +0 -146
  137. package/examples/state-level.json +0 -90136
  138. package/examples/state-points.json +0 -10474
  139. package/examples/temp-example-data.json +0 -130
  140. package/examples/test-dashboard-simple.json +0 -503
  141. package/examples/test-example.json +0 -752
  142. package/examples/test-file.json +0 -147
  143. package/examples/test.json +0 -752
  144. package/examples/testing.json +0 -94456
  145. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  146. /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
  147. /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
  148. /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
  149. /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
  150. /package/examples/api-test/{years.json → __data__/years.json} +0 -0
  151. /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
@@ -0,0 +1,196 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, userEvent, within } from 'storybook/test'
3
+ import {
4
+ assertVisualizationRendered,
5
+ getDisplayValue,
6
+ performAndAssert,
7
+ waitForOptionsToPopulate
8
+ } from '@cdc/core/helpers/testing'
9
+ import Dashboard from '../CdcDashboard'
10
+ import StaleDatasetKeysConfig from '../../examples/dashboard-stale-dataset-keys.json'
11
+ import MissingDatasetsSingleConfig from '../../examples/dashboard-missing-datasets-single.json'
12
+ import MissingDatasetsMultiConfig from '../../examples/dashboard-missing-datasets-multi.json'
13
+ import TieredFilterConfig from '../../examples/dashboard-tiered-filter-regression.json'
14
+ import MultiDashboardVersionConfig from '../../examples/dashboard-multi-dashboard-version-regression.json'
15
+
16
+ const meta: Meta<typeof Dashboard> = {
17
+ title: 'Components/Pages/Dashboard/Regression Smoke',
18
+ component: Dashboard
19
+ }
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof Dashboard>
23
+
24
+ const expectNoCrashText = (canvasElement: HTMLElement) => {
25
+ expect(canvasElement.textContent).not.toContain('Cannot read properties of undefined')
26
+ expect(canvasElement.textContent).not.toContain('Something went wrong')
27
+ }
28
+
29
+ export const Stale_Dataset_Keys_Are_Skipped_Safely: Story = {
30
+ args: {
31
+ config: StaleDatasetKeysConfig,
32
+ isEditor: false
33
+ },
34
+ play: async ({ canvasElement }) => {
35
+ await assertVisualizationRendered(canvasElement)
36
+ expectNoCrashText(canvasElement)
37
+ expect(canvasElement.textContent).toContain('How to use this fixture')
38
+ expect(canvasElement.textContent).toContain('Valid viz.dataKey')
39
+ expect(getDisplayValue(canvasElement)).toContain('123')
40
+ }
41
+ }
42
+
43
+ export const Missing_Datasets_Single_Dashboard_Loads_Safely: Story = {
44
+ args: {
45
+ config: MissingDatasetsSingleConfig,
46
+ isEditor: false
47
+ },
48
+ play: async ({ canvasElement }) => {
49
+ await assertVisualizationRendered(canvasElement)
50
+ expectNoCrashText(canvasElement)
51
+ expect(canvasElement.textContent).toContain('Fixture notes')
52
+ expect(canvasElement.textContent).toContain('Missing dataset')
53
+ expect(canvasElement.textContent).toContain('Empty dataset')
54
+ }
55
+ }
56
+
57
+ export const Missing_Datasets_Multi_Dashboard_Loads_Safely: Story = {
58
+ args: {
59
+ config: MissingDatasetsMultiConfig,
60
+ isEditor: false
61
+ },
62
+ play: async ({ canvasElement }) => {
63
+ const canvas = within(canvasElement)
64
+ const user = userEvent.setup()
65
+
66
+ await assertVisualizationRendered(canvasElement)
67
+ expectNoCrashText(canvasElement)
68
+ expect(canvasElement.textContent).toContain('This tab has no datasets object entries for its chart.')
69
+
70
+ const emptyDatasetTab = canvas.getByRole('link', { name: 'Empty Dataset' })
71
+ await user.click(emptyDatasetTab)
72
+
73
+ await performAndAssert(
74
+ 'Switch to multi-dashboard tab with empty dataset',
75
+ () => canvasElement.textContent || '',
76
+ async () => {},
77
+ (_before, after) => after.includes('This sub-dashboard has a datasets object, but the dataset is empty.')
78
+ )
79
+
80
+ expectNoCrashText(canvasElement)
81
+ expect(canvasElement.textContent).toContain('Please complete your selection to continue.')
82
+ }
83
+ }
84
+
85
+ export const Tiered_Filtering_Applies_Sequentially: Story = {
86
+ args: {
87
+ config: TieredFilterConfig,
88
+ isEditor: false
89
+ },
90
+ play: async ({ canvasElement }) => {
91
+ const canvas = within(canvasElement)
92
+ const user = userEvent.setup()
93
+
94
+ const regionFilter = (await canvas.findByLabelText('Region', { selector: 'select' })) as HTMLSelectElement
95
+ const categoryFilter = (await canvas.findByLabelText('Category', { selector: 'select' })) as HTMLSelectElement
96
+ const detailFilter = (await canvas.findByLabelText('Detail', { selector: 'select' })) as HTMLSelectElement
97
+
98
+ const getOptions = (select: HTMLSelectElement) =>
99
+ Array.from(select.options)
100
+ .map(option => option.value)
101
+ .filter(Boolean)
102
+
103
+ const getState = () => ({
104
+ regionSelected: regionFilter.value,
105
+ categorySelected: categoryFilter.value,
106
+ detailSelected: detailFilter.value,
107
+ categoryOptions: getOptions(categoryFilter),
108
+ detailOptions: getOptions(detailFilter),
109
+ svgCount: canvasElement.querySelectorAll('svg').length,
110
+ noDataVisible: !!canvas.queryByText('No Data Available')
111
+ })
112
+
113
+ await waitForOptionsToPopulate(regionFilter, 3)
114
+ await waitForOptionsToPopulate(categoryFilter, 3)
115
+ await waitForOptionsToPopulate(detailFilter, 3)
116
+
117
+ const initialState = getState()
118
+ expect(initialState.regionSelected).toBe('North')
119
+ expect(initialState.categorySelected).toBe('Alpha')
120
+ expect(initialState.detailSelected).toBe('One')
121
+ expect(initialState.svgCount).toBeGreaterThan(0)
122
+ expect(initialState.noDataVisible).toBe(false)
123
+
124
+ await performAndAssert(
125
+ 'Change tier-1 filter and repopulate lower tiers',
126
+ getState,
127
+ async () => await user.selectOptions(regionFilter, ['South']),
128
+ (_before, after) =>
129
+ after.regionSelected === 'South' &&
130
+ after.categoryOptions.includes('Alpha') &&
131
+ after.categoryOptions.includes('Beta') &&
132
+ after.categoryOptions.includes(after.categorySelected) &&
133
+ after.detailOptions.includes(after.detailSelected) &&
134
+ after.svgCount > 0 &&
135
+ !after.noDataVisible
136
+ )
137
+
138
+ await performAndAssert(
139
+ 'Change tier-2 filter and observe the intermediate no-data state',
140
+ getState,
141
+ async () => await user.selectOptions(categoryFilter, ['Beta']),
142
+ (_before, after) =>
143
+ after.regionSelected === 'South' &&
144
+ after.categorySelected === 'Beta' &&
145
+ after.detailSelected === 'One' &&
146
+ after.noDataVisible
147
+ )
148
+
149
+ await performAndAssert(
150
+ 'Change tier-3 filter to restore data after tier-2 change',
151
+ getState,
152
+ async () => await user.selectOptions(detailFilter, ['Two']),
153
+ (_before, after) =>
154
+ after.regionSelected === 'South' &&
155
+ after.categorySelected === 'Beta' &&
156
+ after.detailSelected === 'Two' &&
157
+ after.svgCount > 0 &&
158
+ !after.noDataVisible
159
+ )
160
+ }
161
+ }
162
+
163
+ export const Multi_Dashboard_Versioning_Remains_Stable: Story = {
164
+ args: {
165
+ config: MultiDashboardVersionConfig,
166
+ isEditor: false
167
+ },
168
+ play: async ({ canvasElement }) => {
169
+ const canvas = within(canvasElement)
170
+ const user = userEvent.setup()
171
+
172
+ await assertVisualizationRendered(canvasElement)
173
+ expectNoCrashText(canvasElement)
174
+
175
+ await user.click(canvas.getByRole('link', { name: 'Explicit Version' }))
176
+
177
+ await performAndAssert(
178
+ 'Switch to explicit-version sub-dashboard',
179
+ () => canvasElement.textContent || '',
180
+ async () => {},
181
+ (_before, after) => after.includes('Because this sub-dashboard is already at')
182
+ )
183
+
184
+ await user.click(canvas.getByRole('link', { name: 'Fallback Version' }))
185
+
186
+ await performAndAssert(
187
+ 'Switch to fallback-version sub-dashboard',
188
+ () => canvasElement.textContent || '',
189
+ async () => {},
190
+ (_before, after) => after.includes('This tab intentionally omits its own version.')
191
+ )
192
+
193
+ expectNoCrashText(canvasElement)
194
+ expect(canvasElement.textContent).toContain('Fallback version tab')
195
+ }
196
+ }
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, userEvent } from 'storybook/test'
3
+ import cloneDeep from 'lodash/cloneDeep'
4
+ import Dashboard from '../CdcDashboard'
5
+ import SingleStateDashboardFilters from './_mock/single-state-dashboard-filters.json'
6
+ import {
7
+ assertVisualizationRendered,
8
+ performAndAssert,
9
+ waitForOptionsToPopulate,
10
+ waitForPresence
11
+ } from '@cdc/core/helpers/testing'
12
+
13
+ type Story = StoryObj<typeof Dashboard>
14
+
15
+ const meta: Meta<typeof Dashboard> = {
16
+ title: 'Components/Pages/Dashboard/Zoom',
17
+ component: Dashboard,
18
+ parameters: {
19
+ layout: 'fullscreen'
20
+ }
21
+ }
22
+
23
+ export default meta
24
+
25
+ const SingleStateDashboardZoomResetConfig = cloneDeep(SingleStateDashboardFilters)
26
+ SingleStateDashboardZoomResetConfig.dashboard.title = 'Single-State Shared Filter Zoom Reset'
27
+ SingleStateDashboardZoomResetConfig.dashboard.sharedFilters[0].key = 'State'
28
+ SingleStateDashboardZoomResetConfig.dashboard.sharedFilters[0].active = 'California'
29
+ SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.title =
30
+ 'Single-State Shared Filter Zoom Reset'
31
+ SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.filterControlsStatesPicked = 'State'
32
+ SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.statesPicked = [
33
+ {
34
+ fipsCode: '06',
35
+ stateName: 'California'
36
+ }
37
+ ]
38
+ delete SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.filterControlsStatePicked
39
+ delete SingleStateDashboardZoomResetConfig.visualizations.map1721943918271.general.statePicked
40
+
41
+ const readZoomTransform = (canvasElement: HTMLElement) => {
42
+ const zoomLayer = canvasElement.querySelector('svg > g > g[transform]') as SVGGElement | null
43
+ return zoomLayer?.getAttribute('transform') || ''
44
+ }
45
+
46
+ export const SingleStateSharedFilterReset: Story = {
47
+ args: {
48
+ config: SingleStateDashboardZoomResetConfig,
49
+ isEditor: false
50
+ },
51
+ parameters: {
52
+ docs: {
53
+ description: {
54
+ story:
55
+ 'Dashboard zoom regression story for DEV-13086. Zoom the single-state map, then change the shared State filter. The map should refit to the new single state instead of preserving the prior zoom.'
56
+ }
57
+ }
58
+ },
59
+ play: async ({ canvasElement }) => {
60
+ const canvas = within(canvasElement)
61
+
62
+ await assertVisualizationRendered(canvasElement)
63
+
64
+ const stateFilter = canvas.getByLabelText('State', { selector: 'select' }) as HTMLSelectElement
65
+ await waitForOptionsToPopulate(stateFilter, 2)
66
+ await waitForPresence('button[aria-label="Zoom In"]', canvasElement)
67
+
68
+ const zoomInButton = canvas.getByLabelText('Zoom In')
69
+
70
+ await performAndAssert(
71
+ 'Single-state dashboard map zooms in',
72
+ () => readZoomTransform(canvasElement),
73
+ async () => {
74
+ await userEvent.click(zoomInButton)
75
+ },
76
+ (before, after) => before !== after && !after.includes('scale(1)')
77
+ )
78
+
79
+ await performAndAssert(
80
+ 'Shared filter change refits single-state zoom',
81
+ () => readZoomTransform(canvasElement),
82
+ async () => {
83
+ await userEvent.selectOptions(stateFilter, ['Florida'])
84
+ },
85
+ (before, after) => before !== after && after.includes('scale(1)')
86
+ )
87
+ }
88
+ }
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect } from 'storybook/test'
3
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
4
+ import Dashboard from '../CdcDashboard'
5
+ import MinimalExampleConfig from '../../examples/minimal-example.json'
6
+
7
+ const meta: Meta<typeof Dashboard> = {
8
+ title: 'Components/Pages/Dashboard',
9
+ component: Dashboard
10
+ }
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof Dashboard>
14
+
15
+ export const Dashboard_Minimal_Config: Story = {
16
+ args: {
17
+ config: MinimalExampleConfig,
18
+ isEditor: false
19
+ },
20
+ parameters: {
21
+ docs: {
22
+ description: {
23
+ story:
24
+ 'Minimum working consumer config. This story validates the source-of-truth minimal example used by the package README and CONFIG reference.'
25
+ }
26
+ }
27
+ },
28
+ play: async ({ canvasElement }) => {
29
+ await assertVisualizationRendered(canvasElement)
30
+ expect(canvasElement.textContent).toContain('Dashboard Example')
31
+ expect(canvasElement.textContent).toContain('Minimal dashboard example')
32
+ }
33
+ }
@@ -44,13 +44,193 @@ import GalleryDataBiteDashboard from './_mock/gallery-data-bite-dashboard.json'
44
44
  import TP5TestConfig from './_mock/tp5-test.json'
45
45
  import LineChartAnglesConfig from './_mock/dashboard-line-chart-angles.json'
46
46
  import TabSimpleFilterConfig from './_mock/tab-simple-filter.json'
47
+ import DataDrivenColorsConfig from './_mock/dashboard-data-driven-colors.json'
47
48
 
48
49
  // Dashboard Filter Updates for Ascending, Descending, and Custom Order
49
50
  import DashboardFilterAsc from './_mock/dashboard-filter-asc.json'
50
51
  const DashboardFilterDesc = cloneDeep(DashboardFilterAsc)
51
52
  const DashboardFilterCust = cloneDeep(DashboardFilterAsc)
53
+ const NestedParentChildFiltersSubgroupOnly = cloneDeep(NestedParentChildFilters)
52
54
  DashboardFilterDesc.dashboard.sharedFilters[0].order = 'desc'
53
55
  DashboardFilterCust.dashboard.sharedFilters[0].order = 'cust'
56
+ NestedParentChildFiltersSubgroupOnly.dashboard.sharedFilters[1].displaySubgroupingOnly = true
57
+
58
+ const DashboardConditionsConfig: Config = {
59
+ type: 'dashboard',
60
+ version: '4.26.4',
61
+ dashboard: {
62
+ theme: 'theme-blue',
63
+ title: 'Dashboard Conditions',
64
+ titleStyle: 'small',
65
+ sharedFilters: [
66
+ {
67
+ key: 'Required Selection',
68
+ type: 'datafilter',
69
+ filterStyle: 'dropdown',
70
+ columnName: 'selection',
71
+ showDropdown: true,
72
+ usedBy: ['markup-filters-incomplete'],
73
+ values: ['Ready'],
74
+ resetLabel: 'Select a value',
75
+ active: 'Select a value',
76
+ order: 'asc',
77
+ parents: []
78
+ },
79
+ {
80
+ key: 'Availability',
81
+ type: 'datafilter',
82
+ filterStyle: 'dropdown',
83
+ columnName: 'availability',
84
+ showDropdown: true,
85
+ usedBy: [2],
86
+ values: ['Show', 'Hide'],
87
+ active: 'Show',
88
+ order: 'asc',
89
+ parents: []
90
+ },
91
+ {
92
+ key: 'Region',
93
+ type: 'datafilter',
94
+ filterStyle: 'dropdown',
95
+ columnName: 'region',
96
+ showDropdown: true,
97
+ usedBy: ['markup-condition-a'],
98
+ values: ['East', 'West'],
99
+ active: 'East',
100
+ order: 'asc',
101
+ parents: []
102
+ }
103
+ ]
104
+ },
105
+ data: [],
106
+ datasets: {
107
+ 'visual-data': {
108
+ data: [{ title: 'Visualization dataset' }]
109
+ },
110
+ 'row-condition-data': {
111
+ data: [{ availability: 'Show' }]
112
+ },
113
+ 'column-condition-data': {
114
+ data: [
115
+ { region: 'East', visibility: 1 },
116
+ { region: 'West', visibility: 0 }
117
+ ]
118
+ }
119
+ },
120
+ rows: [
121
+ {
122
+ columns: [{ width: 12, widget: 'dashboard-filters-conditions' }],
123
+ expandCollapseAllButtons: false
124
+ },
125
+ {
126
+ columns: [
127
+ {
128
+ width: 12,
129
+ conditionalWidgets: [
130
+ {
131
+ widget: 'markup-filters-incomplete',
132
+ dashboardCondition: {
133
+ id: 'filters-incomplete-story',
134
+ operator: 'filtersIncomplete'
135
+ }
136
+ }
137
+ ]
138
+ }
139
+ ],
140
+ expandCollapseAllButtons: false
141
+ },
142
+ {
143
+ dashboardCondition: {
144
+ id: 'row-condition-story',
145
+ datasetKey: 'row-condition-data',
146
+ operator: 'hasData'
147
+ },
148
+ columns: [
149
+ {
150
+ width: 6,
151
+ conditionalWidgets: [
152
+ {
153
+ widget: 'markup-condition-a',
154
+ dashboardCondition: {
155
+ id: 'column-condition-story',
156
+ datasetKey: 'column-condition-data',
157
+ operator: 'columnHasAnyValue',
158
+ columnName: 'visibility',
159
+ values: ['1']
160
+ }
161
+ }
162
+ ]
163
+ },
164
+ {
165
+ width: 6,
166
+ widget: 'markup-condition-b'
167
+ }
168
+ ],
169
+ expandCollapseAllButtons: false
170
+ }
171
+ ],
172
+ visualizations: {
173
+ 'dashboard-filters-conditions': {
174
+ uid: 'dashboard-filters-conditions',
175
+ type: 'dashboardFilters',
176
+ visualizationType: 'dashboardFilters',
177
+ sharedFilterIndexes: [0, 1, 2],
178
+ filterBehavior: 'Filter Change',
179
+ filters: [],
180
+ autoLoad: true
181
+ },
182
+ 'markup-filters-incomplete': {
183
+ uid: 'markup-filters-incomplete',
184
+ type: 'markup-include',
185
+ visualizationType: 'markup-include',
186
+ filterBehavior: 'Filter Change',
187
+ theme: 'theme-blue',
188
+ contentEditor: {
189
+ inlineHTML:
190
+ '<p>This authored module appears because <strong>Required Selection</strong> starts incomplete. Choose Ready to hide it.</p>',
191
+ showHeader: true,
192
+ srcUrl: '',
193
+ title: 'Filters Incomplete Condition',
194
+ useInlineHTML: true
195
+ }
196
+ },
197
+ 'markup-condition-a': {
198
+ uid: 'markup-condition-a',
199
+ type: 'markup-include',
200
+ visualizationType: 'markup-include',
201
+ dataKey: 'visual-data',
202
+ filterBehavior: 'Filter Change',
203
+ theme: 'theme-blue',
204
+ contentEditor: {
205
+ inlineHTML:
206
+ '<p>This column stays visible while <strong>Region</strong> is East and hides while keeping its width when Region is West.</p>',
207
+ showHeader: true,
208
+ srcUrl: '',
209
+ title: 'Component-Level Condition',
210
+ useInlineHTML: true
211
+ }
212
+ },
213
+ 'markup-condition-b': {
214
+ uid: 'markup-condition-b',
215
+ type: 'markup-include',
216
+ visualizationType: 'markup-include',
217
+ dataKey: 'visual-data',
218
+ filterBehavior: 'Filter Change',
219
+ theme: 'theme-blue',
220
+ contentEditor: {
221
+ inlineHTML:
222
+ '<p>This companion column stays rendered so you can see row-level hiding separately from component-level hiding.</p>',
223
+ showHeader: true,
224
+ srcUrl: '',
225
+ title: 'Always Visible Companion',
226
+ useInlineHTML: true
227
+ }
228
+ }
229
+ },
230
+ table: {
231
+ show: false
232
+ }
233
+ }
54
234
 
55
235
  // On DashboardFilterCust change the sharedFilters[0].values and orderedValues to be in a custom order
56
236
  const customOrder = ['American Samoa', 'Alaska', 'Alabama', 'Arizona', 'Arkansas']
@@ -112,6 +292,123 @@ export const Example_3: Story = {
112
292
  }
113
293
  }
114
294
 
295
+ export const Dashboard_Conditions: Story = {
296
+ args: {
297
+ config: DashboardConditionsConfig,
298
+ isEditor: false
299
+ },
300
+ play: async ({ canvasElement }) => {
301
+ const canvas = within(canvasElement)
302
+ const user = userEvent.setup()
303
+
304
+ const incompleteTitle = 'Filters Incomplete Condition'
305
+ const componentTitle = 'Component-Level Condition'
306
+ const companionTitle = 'Always Visible Companion'
307
+ const legacyIncompleteMessage = 'Please complete your selection to continue.'
308
+
309
+ const availabilityFilter = (await canvas.findByLabelText('Availability', {
310
+ selector: 'select'
311
+ })) as HTMLSelectElement
312
+ const regionFilter = (await canvas.findByLabelText('Region', { selector: 'select' })) as HTMLSelectElement
313
+ const requiredSelectionFilter = (await canvas.findByLabelText('Required Selection', {
314
+ selector: 'select'
315
+ })) as HTMLSelectElement
316
+
317
+ await waitForOptionsToPopulate(requiredSelectionFilter, 2)
318
+
319
+ const getState = () => {
320
+ const visibleText = canvasElement.innerText
321
+ return {
322
+ availabilitySelected: availabilityFilter.value,
323
+ regionSelected: regionFilter.value,
324
+ requiredSelectionSelected: requiredSelectionFilter.value,
325
+ incompleteVisible: visibleText.includes(incompleteTitle),
326
+ componentVisible: visibleText.includes(componentTitle),
327
+ companionVisible: visibleText.includes(companionTitle),
328
+ legacyIncompleteVisible: visibleText.includes(legacyIncompleteMessage)
329
+ }
330
+ }
331
+
332
+ const initialState = getState()
333
+ expect(initialState.incompleteVisible).toBe(true)
334
+ expect(initialState.componentVisible).toBe(false)
335
+ expect(initialState.companionVisible).toBe(false)
336
+ expect(initialState.legacyIncompleteVisible).toBe(false)
337
+
338
+ await performAndAssert(
339
+ 'Complete required filter -> row and component conditions render',
340
+ getState,
341
+ async () => await user.selectOptions(requiredSelectionFilter, ['Ready']),
342
+ (_before, after) =>
343
+ after.requiredSelectionSelected === 'Ready' &&
344
+ !after.incompleteVisible &&
345
+ after.componentVisible &&
346
+ after.companionVisible &&
347
+ !after.legacyIncompleteVisible
348
+ )
349
+
350
+ await performAndAssert(
351
+ 'Select Region=West -> component-level condition hides only the conditioned component',
352
+ getState,
353
+ async () => await user.selectOptions(regionFilter, ['West']),
354
+ (_before, after) =>
355
+ after.regionSelected === 'West' &&
356
+ !after.incompleteVisible &&
357
+ !after.componentVisible &&
358
+ after.companionVisible &&
359
+ !after.legacyIncompleteVisible
360
+ )
361
+
362
+ await performAndAssert(
363
+ 'Select Region=East -> component-level condition shows the conditioned component',
364
+ getState,
365
+ async () => await user.selectOptions(regionFilter, ['East']),
366
+ (_before, after) =>
367
+ after.regionSelected === 'East' &&
368
+ !after.incompleteVisible &&
369
+ after.componentVisible &&
370
+ after.companionVisible &&
371
+ !after.legacyIncompleteVisible
372
+ )
373
+
374
+ await performAndAssert(
375
+ 'Select Availability=Hide -> row-level condition hides the full conditioned row',
376
+ getState,
377
+ async () => await user.selectOptions(availabilityFilter, ['Hide']),
378
+ (_before, after) =>
379
+ after.availabilitySelected === 'Hide' &&
380
+ !after.incompleteVisible &&
381
+ !after.componentVisible &&
382
+ !after.companionVisible &&
383
+ !after.legacyIncompleteVisible
384
+ )
385
+
386
+ await performAndAssert(
387
+ 'Select Availability=Show -> row-level condition shows the full conditioned row',
388
+ getState,
389
+ async () => await user.selectOptions(availabilityFilter, ['Show']),
390
+ (_before, after) =>
391
+ after.availabilitySelected === 'Show' &&
392
+ !after.incompleteVisible &&
393
+ after.componentVisible &&
394
+ after.companionVisible &&
395
+ !after.legacyIncompleteVisible
396
+ )
397
+
398
+ await performAndAssert(
399
+ 'Reset required filter -> authored filtersIncomplete module returns',
400
+ getState,
401
+ async () => await user.selectOptions(requiredSelectionFilter, ['Select a value']),
402
+ (_before, after) =>
403
+ after.requiredSelectionSelected === 'Select a value' &&
404
+ after.incompleteVisible &&
405
+ !after.componentVisible &&
406
+ !after.companionVisible &&
407
+ !after.legacyIncompleteVisible
408
+ )
409
+ }
410
+ }
411
+
115
412
  export const TP5_Test_Dashboard: Story = {
116
413
  args: {
117
414
  config: TP5TestConfig,
@@ -119,6 +416,20 @@ export const TP5_Test_Dashboard: Story = {
119
416
  }
120
417
  }
121
418
 
419
+ export const DataDrivenColors: Story = {
420
+ args: {
421
+ config: DataDrivenColorsConfig as unknown as Config,
422
+ isEditor: false
423
+ }
424
+ }
425
+
426
+ export const DataDrivenColors_Editor: Story = {
427
+ args: {
428
+ config: DataDrivenColorsConfig as unknown as Config,
429
+ isEditor: true
430
+ }
431
+ }
432
+
122
433
  export const Line_Chart_Angles: Story = {
123
434
  args: {
124
435
  config: LineChartAnglesConfig,
@@ -4788,8 +5099,7 @@ export const Nested_Dropdown_With_Parent_Child: Story = {
4788
5099
 
4789
5100
  // Verify defaultValue is applied on initial load (North region, 2023 Q2)
4790
5101
  expect(initialState.regionSelected).toBe('North')
4791
- expect(initialState.yearQuarterSelected).toContain('2023')
4792
- expect(initialState.yearQuarterSelected).toContain('Q2')
5102
+ expect(initialState.yearQuarterSelected).toBe('2023 - Q2')
4793
5103
  expect(initialState.chartRendered).toBe(true)
4794
5104
 
4795
5105
  // Test 1: Change region to South → year options should update based on available data
@@ -4846,6 +5156,31 @@ export const Nested_Dropdown_With_Parent_Child: Story = {
4846
5156
  }
4847
5157
  }
4848
5158
 
5159
+ export const Nested_Dropdown_With_Parent_Child_Subgroup_Only: Story = {
5160
+ args: {
5161
+ config: NestedParentChildFiltersSubgroupOnly as unknown as Config,
5162
+ isEditor: false
5163
+ },
5164
+ parameters: {
5165
+ docs: {
5166
+ description: {
5167
+ story:
5168
+ 'Displays only the selected subgroup in the closed nested dropdown input while preserving the same filter behavior.'
5169
+ }
5170
+ }
5171
+ },
5172
+ play: async ({ canvasElement }) => {
5173
+ const canvas = within(canvasElement)
5174
+ const regionFilter = (await canvas.findByLabelText('Region', { selector: 'select' })) as HTMLSelectElement
5175
+ const yearQuarterInput = canvasElement.querySelector('.nested-dropdown input') as HTMLInputElement
5176
+
5177
+ await waitForOptionsToPopulate(regionFilter, 4)
5178
+
5179
+ expect(regionFilter.value).toBe('North')
5180
+ expect(yearQuarterInput.value).toBe('Q2')
5181
+ }
5182
+ }
5183
+
4849
5184
  export const Metadata_In_Dashboard: Story = {
4850
5185
  args: {
4851
5186
  configUrl: '/packages/dashboard/tests/fixtures/dashboard-config-with-metadata.json'