@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.
- package/CONFIG.md +77 -30
- package/LICENSE +201 -0
- package/dist/cdcdashboard.js +49936 -49166
- package/examples/dashboard-conditions-filters-incomplete.json +221 -0
- package/examples/dashboard-missing-datasets-multi.json +174 -0
- package/examples/dashboard-missing-datasets-single.json +121 -0
- package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
- package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
- package/examples/dashboard-stale-dataset-keys.json +181 -0
- package/examples/dashboard-tiered-filter-regression.json +190 -0
- package/examples/private/cfa-dashboard.json +651 -0
- package/examples/private/data-bite-wrap.json +6936 -0
- package/examples/private/multi-dash-fix.json +16963 -0
- package/examples/private/versions.json +41612 -0
- package/examples/us-map-filter-example.json +1074 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +6 -2
- package/src/CdcDashboardComponent.tsx +178 -87
- package/src/DashboardCopyPasteContext.test.tsx +33 -0
- package/src/DashboardCopyPasteContext.tsx +48 -0
- package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
- package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
- package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
- package/src/_stories/Dashboard.stories.tsx +294 -0
- package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
- package/src/components/Column.test.tsx +176 -0
- package/src/components/Column.tsx +214 -13
- package/src/components/DashboardConditionModal.test.tsx +420 -0
- package/src/components/DashboardConditionModal.tsx +367 -0
- package/src/components/DashboardConditionSummary.tsx +59 -0
- package/src/components/DashboardEditors.tsx +8 -0
- package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
- package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
- package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
- package/src/components/DataDesignerModal.tsx +2 -1
- package/src/components/Grid.tsx +8 -4
- package/src/components/Header/Header.tsx +36 -17
- package/src/components/Row.test.tsx +228 -0
- package/src/components/Row.tsx +93 -18
- package/src/components/VisualizationRow.test.tsx +396 -0
- package/src/components/VisualizationRow.tsx +110 -35
- package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
- package/src/components/Widget/Widget.test.tsx +218 -0
- package/src/components/Widget/Widget.tsx +119 -17
- package/src/components/Widget/widget.styles.css +31 -18
- package/src/components/dashboard-condition-modal.css +76 -0
- package/src/components/dashboard-condition-summary.css +87 -0
- package/src/helpers/addValuesToDashboardFilters.ts +3 -5
- package/src/helpers/addVisualization.ts +15 -4
- package/src/helpers/cloneDashboardWidget.ts +127 -0
- package/src/helpers/dashboardColumnWidgets.ts +99 -0
- package/src/helpers/dashboardConditionUi.ts +47 -0
- package/src/helpers/dashboardConditions.ts +200 -0
- package/src/helpers/dashboardFilterTargets.ts +156 -0
- package/src/helpers/filterData.ts +4 -9
- package/src/helpers/filterVisibility.ts +20 -0
- package/src/helpers/formatConfigBeforeSave.ts +2 -2
- package/src/helpers/getFilteredData.ts +18 -5
- package/src/helpers/getUpdateConfig.ts +43 -12
- package/src/helpers/getVizRowColumnLocator.ts +11 -1
- package/src/helpers/iconHash.tsx +9 -3
- package/src/helpers/mapDataToConfig.ts +31 -29
- package/src/helpers/reloadURLHelpers.ts +25 -5
- package/src/helpers/removeDashboardFilter.ts +33 -33
- package/src/helpers/tests/addVisualization.test.ts +53 -9
- package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
- package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
- package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
- package/src/helpers/tests/dashboardConditions.test.ts +428 -0
- package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
- package/src/helpers/tests/getFilteredData.test.ts +265 -86
- package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
- package/src/index.tsx +6 -3
- package/src/scss/grid.scss +249 -20
- package/src/scss/main.scss +108 -29
- package/src/store/dashboard.actions.ts +17 -4
- package/src/store/dashboard.reducer.test.ts +538 -0
- package/src/store/dashboard.reducer.ts +135 -22
- package/src/test/CdcDashboard.test.tsx +148 -0
- package/src/test/CdcDashboardComponent.test.tsx +935 -2
- package/src/types/ConfigRow.ts +15 -0
- package/src/types/DashboardFilters.ts +4 -0
- 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
|