@cdc/dashboard 4.26.4 → 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 (91) hide show
  1. package/CONFIG.md +77 -30
  2. package/LICENSE +201 -0
  3. package/dist/cdcdashboard.js +49936 -49166
  4. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  5. package/examples/dashboard-missing-datasets-multi.json +174 -0
  6. package/examples/dashboard-missing-datasets-single.json +121 -0
  7. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  8. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  9. package/examples/dashboard-stale-dataset-keys.json +181 -0
  10. package/examples/dashboard-tiered-filter-regression.json +190 -0
  11. package/examples/private/cfa-dashboard.json +651 -0
  12. package/examples/private/data-bite-wrap.json +6936 -0
  13. package/examples/private/multi-dash-fix.json +16963 -0
  14. package/examples/private/versions.json +41612 -0
  15. package/examples/us-map-filter-example.json +1074 -0
  16. package/package.json +9 -9
  17. package/src/CdcDashboard.tsx +6 -2
  18. package/src/CdcDashboardComponent.tsx +178 -87
  19. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  20. package/src/DashboardCopyPasteContext.tsx +48 -0
  21. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  22. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  23. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  24. package/src/_stories/Dashboard.stories.tsx +294 -0
  25. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  26. package/src/components/Column.test.tsx +176 -0
  27. package/src/components/Column.tsx +214 -13
  28. package/src/components/DashboardConditionModal.test.tsx +420 -0
  29. package/src/components/DashboardConditionModal.tsx +367 -0
  30. package/src/components/DashboardConditionSummary.tsx +59 -0
  31. package/src/components/DashboardEditors.tsx +8 -0
  32. package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
  33. package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
  34. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  35. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
  36. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
  37. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
  38. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  39. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  40. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  41. package/src/components/DataDesignerModal.tsx +2 -1
  42. package/src/components/Grid.tsx +8 -4
  43. package/src/components/Header/Header.tsx +36 -17
  44. package/src/components/Row.test.tsx +228 -0
  45. package/src/components/Row.tsx +93 -18
  46. package/src/components/VisualizationRow.test.tsx +396 -0
  47. package/src/components/VisualizationRow.tsx +110 -35
  48. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  49. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  50. package/src/components/Widget/Widget.test.tsx +218 -0
  51. package/src/components/Widget/Widget.tsx +119 -17
  52. package/src/components/Widget/widget.styles.css +31 -18
  53. package/src/components/dashboard-condition-modal.css +76 -0
  54. package/src/components/dashboard-condition-summary.css +87 -0
  55. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  56. package/src/helpers/addVisualization.ts +15 -4
  57. package/src/helpers/cloneDashboardWidget.ts +127 -0
  58. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  59. package/src/helpers/dashboardConditionUi.ts +47 -0
  60. package/src/helpers/dashboardConditions.ts +200 -0
  61. package/src/helpers/dashboardFilterTargets.ts +156 -0
  62. package/src/helpers/filterData.ts +4 -9
  63. package/src/helpers/filterVisibility.ts +20 -0
  64. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  65. package/src/helpers/getFilteredData.ts +18 -5
  66. package/src/helpers/getUpdateConfig.ts +43 -12
  67. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  68. package/src/helpers/iconHash.tsx +9 -3
  69. package/src/helpers/mapDataToConfig.ts +31 -29
  70. package/src/helpers/reloadURLHelpers.ts +25 -5
  71. package/src/helpers/removeDashboardFilter.ts +33 -33
  72. package/src/helpers/tests/addVisualization.test.ts +53 -9
  73. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  74. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  75. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  76. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  77. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  78. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  79. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  80. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  81. package/src/index.tsx +6 -3
  82. package/src/scss/grid.scss +249 -20
  83. package/src/scss/main.scss +108 -29
  84. package/src/store/dashboard.actions.ts +17 -4
  85. package/src/store/dashboard.reducer.test.ts +538 -0
  86. package/src/store/dashboard.reducer.ts +135 -22
  87. package/src/test/CdcDashboard.test.tsx +148 -0
  88. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  89. package/src/types/ConfigRow.ts +15 -0
  90. package/src/types/DashboardFilters.ts +4 -0
  91. package/src/types/SharedFilter.ts +1 -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
+ }
@@ -55,6 +55,183 @@ DashboardFilterDesc.dashboard.sharedFilters[0].order = 'desc'
55
55
  DashboardFilterCust.dashboard.sharedFilters[0].order = 'cust'
56
56
  NestedParentChildFiltersSubgroupOnly.dashboard.sharedFilters[1].displaySubgroupingOnly = true
57
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
+ }
234
+
58
235
  // On DashboardFilterCust change the sharedFilters[0].values and orderedValues to be in a custom order
59
236
  const customOrder = ['American Samoa', 'Alaska', 'Alabama', 'Arizona', 'Arkansas']
60
237
  DashboardFilterCust.dashboard.sharedFilters[0].orderedValues = customOrder
@@ -115,6 +292,123 @@ export const Example_3: Story = {
115
292
  }
116
293
  }
117
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
+
118
412
  export const TP5_Test_Dashboard: Story = {
119
413
  args: {
120
414
  config: TP5TestConfig,
@@ -0,0 +1,87 @@
1
+ import React from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import CdcFilteredText from '@cdc/filtered-text/src/CdcFilteredText'
4
+ import CdcMarkupInclude from '@cdc/markup-include/src/CdcMarkupInclude'
5
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+ import { expect, waitFor } from 'storybook/test'
7
+
8
+ const comparisonData = [
9
+ {
10
+ State: 'CA',
11
+ Message: 'Representative filtered text output'
12
+ }
13
+ ]
14
+
15
+ const meta: Meta = {
16
+ title: 'Components/Pages/Dashboard/Filtered Text Migration Comparison'
17
+ }
18
+
19
+ type Story = StoryObj
20
+
21
+ export const Standalone_Handoff_And_Migrated_Output: Story = {
22
+ render: () => (
23
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
24
+ <CdcFilteredText
25
+ config={{
26
+ type: 'filtered-text',
27
+ title: 'Legacy filtered text',
28
+ textColumn: 'Message',
29
+ data: comparisonData,
30
+ filters: [{ columnName: 'State', columnValue: 'CA' }]
31
+ }}
32
+ isEditor={false}
33
+ />
34
+ <CdcMarkupInclude
35
+ config={
36
+ {
37
+ type: 'markup-include',
38
+ theme: 'theme-blue',
39
+ data: comparisonData,
40
+ enableMarkupVariables: true,
41
+ markupVariables: [
42
+ {
43
+ sourceType: 'column',
44
+ outputType: 'value',
45
+ name: 'Message',
46
+ tag: '{{message}}',
47
+ columnName: 'Message',
48
+ conditions: [{ columnName: 'State', isOrIsNotEqualTo: 'is', value: 'CA' }],
49
+ selectionMode: 'first'
50
+ }
51
+ ],
52
+ contentEditor: {
53
+ title: 'Migrated markup include',
54
+ inlineHTML: '{{message}}',
55
+ useInlineHTML: true,
56
+ srcUrl: '',
57
+ showHeader: true,
58
+ style: 'default'
59
+ },
60
+ visual: {
61
+ border: false,
62
+ accent: false,
63
+ background: false,
64
+ hideBackgroundColor: false,
65
+ borderColorTheme: false
66
+ }
67
+ } as any
68
+ }
69
+ configUrl=''
70
+ datasets={{}}
71
+ isEditor={false}
72
+ isDashboard={false}
73
+ setConfig={() => {}}
74
+ />
75
+ </div>
76
+ ),
77
+ play: async ({ canvasElement }) => {
78
+ await assertVisualizationRendered(canvasElement)
79
+ await waitFor(() => {
80
+ expect(canvasElement.textContent).toContain('Filtered Text Has Been Migrated')
81
+ expect(canvasElement.textContent).toContain('Use the markup-include package to render this visualization.')
82
+ expect(canvasElement.textContent?.match(/Representative filtered text output/g)?.length).toBe(1)
83
+ })
84
+ }
85
+ }
86
+
87
+ export default meta