@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
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/dashboard",
|
|
3
|
-
"version": "4.26.
|
|
3
|
+
"version": "4.26.5",
|
|
4
4
|
"description": "React component for combining multiple visualizations into a single dashboard",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Matthew Pallansch <mpallansch@adittech.com>",
|
|
7
7
|
"bugs": "https://github.com/CDCgov/cdc-open-viz/issues",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@cdc/chart": "^4.26.
|
|
10
|
-
"@cdc/core": "^4.26.
|
|
11
|
-
"@cdc/data-bite": "^4.26.
|
|
12
|
-
"@cdc/filtered-text": "^4.26.
|
|
13
|
-
"@cdc/map": "^4.26.
|
|
14
|
-
"@cdc/markup-include": "^4.26.
|
|
15
|
-
"@cdc/waffle-chart": "^4.26.
|
|
9
|
+
"@cdc/chart": "^4.26.5",
|
|
10
|
+
"@cdc/core": "^4.26.5",
|
|
11
|
+
"@cdc/data-bite": "^4.26.5",
|
|
12
|
+
"@cdc/filtered-text": "^4.26.5",
|
|
13
|
+
"@cdc/map": "^4.26.5",
|
|
14
|
+
"@cdc/markup-include": "^4.26.5",
|
|
15
|
+
"@cdc/waffle-chart": "^4.26.5",
|
|
16
16
|
"js-base64": "^2.5.2",
|
|
17
17
|
"react-accessible-accordion": "^5.0.1",
|
|
18
18
|
"react-dnd": "^16.0.1",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"vite-plugin-css-injected-by-js": "^2.4.0",
|
|
27
27
|
"vite-plugin-svgr": "^4.2.0"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "61c025165d96b45a6002c34582c5a622a9d865a9",
|
|
30
30
|
"main": "dist/cdcdashboard",
|
|
31
31
|
"moduleName": "CdcDashboard",
|
|
32
32
|
"peerDependencies": {
|
package/src/CdcDashboard.tsx
CHANGED
|
@@ -71,7 +71,8 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({
|
|
|
71
71
|
return acc
|
|
72
72
|
}, {})
|
|
73
73
|
getVizKeys(newConfig).forEach(vizKey => {
|
|
74
|
-
const
|
|
74
|
+
const dataKey = newConfig.visualizations[vizKey].dataKey
|
|
75
|
+
const formattedData = dataKey ? datasets[dataKey] : undefined
|
|
75
76
|
if (formattedData) {
|
|
76
77
|
newConfig.visualizations[vizKey].formattedData = formattedData
|
|
77
78
|
}
|
|
@@ -87,7 +88,7 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({
|
|
|
87
88
|
const loadSingleDashboard = async config => {
|
|
88
89
|
let newConfig = { ...defaults, ...config } as DashboardConfig
|
|
89
90
|
|
|
90
|
-
if (config.datasets) {
|
|
91
|
+
if (config.datasets && Object.keys(config.datasets).length > 0) {
|
|
91
92
|
return prepareDatasets(newConfig)
|
|
92
93
|
} else {
|
|
93
94
|
const dataKey = newConfig.dataFileName || 'backwards-compatibility'
|
|
@@ -141,6 +142,9 @@ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({
|
|
|
141
142
|
multiDashboards: multiConfig.multiDashboards,
|
|
142
143
|
activeDashboard: selectedConfig
|
|
143
144
|
} as MultiDashboardConfig
|
|
145
|
+
if (!newConfig.datasets || Object.keys(newConfig.datasets).length === 0) {
|
|
146
|
+
return { newConfig, datasets: {} }
|
|
147
|
+
}
|
|
144
148
|
return prepareDatasets(newConfig)
|
|
145
149
|
}
|
|
146
150
|
|
|
@@ -46,8 +46,10 @@ import EditorContext from '@cdc/core/contexts/EditorContext'
|
|
|
46
46
|
import { APIFilterDropdowns } from './components/DashboardFilters'
|
|
47
47
|
import { ViewPort } from '@cdc/core/types/ViewPort'
|
|
48
48
|
import VisualizationRow from './components/VisualizationRow'
|
|
49
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
49
50
|
import { getVizConfig } from './helpers/getVizConfig'
|
|
50
51
|
import { getFilteredData } from './helpers/getFilteredData'
|
|
52
|
+
import { dashboardRowsUseFiltersIncomplete } from './helpers/dashboardConditions'
|
|
51
53
|
import { getVizRowColumnLocator } from './helpers/getVizRowColumnLocator'
|
|
52
54
|
import { Responsive, VisualizationContainer } from '@cdc/core/components/Layout'
|
|
53
55
|
import * as reloadURLHelpers from './helpers/reloadURLHelpers'
|
|
@@ -59,7 +61,9 @@ import Alert from '@cdc/core/components/Alert'
|
|
|
59
61
|
import { shouldLoadAllFilters } from './helpers/shouldLoadAllFilters'
|
|
60
62
|
import { subscribe, unsubscribe } from '@cdc/core/helpers/events'
|
|
61
63
|
import DashboardEditors from './components/DashboardEditors'
|
|
64
|
+
import { DashboardCopyPasteProvider } from './DashboardCopyPasteContext'
|
|
62
65
|
import { updateChildFilters } from './helpers/updateChildFilters'
|
|
66
|
+
import { getColumnWidgetEntries } from './helpers/dashboardColumnWidgets'
|
|
63
67
|
|
|
64
68
|
type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
|
|
65
69
|
initialState: InitialState
|
|
@@ -80,8 +84,30 @@ export default function CdcDashboard({
|
|
|
80
84
|
const [allExpanded, setAllExpanded] = useState(true)
|
|
81
85
|
const [apiLoading, setAPILoading] = useState(false)
|
|
82
86
|
|
|
87
|
+
// Capture initial filter values at mount (before user interactions mutate filter.active)
|
|
88
|
+
const initialFilterValues = useMemo(() => {
|
|
89
|
+
const values: Record<string, string> = {}
|
|
90
|
+
for (const filter of initialState.config.dashboard?.sharedFilters || []) {
|
|
91
|
+
if (filter.setBy) {
|
|
92
|
+
const active = Array.isArray(filter.active) ? filter.active[0] : filter.active
|
|
93
|
+
values[filter.setBy] = filter.defaultValue || active || filter.values?.[0] || ''
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return values
|
|
97
|
+
}, [])
|
|
98
|
+
|
|
83
99
|
const isPreview = state.tabSelected === 'Dashboard Preview'
|
|
84
100
|
|
|
101
|
+
const hasFiltersIncompleteCondition = useMemo(
|
|
102
|
+
() => dashboardRowsUseFiltersIncomplete(state.config.rows || []),
|
|
103
|
+
[state.config.rows]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const hasIncompleteSharedFilters = useMemo(() => {
|
|
107
|
+
const sharedFilters = state.config.dashboard?.sharedFilters || []
|
|
108
|
+
return sharedFilters.some(isFilterAtResetState)
|
|
109
|
+
}, [state.config.dashboard?.sharedFilters])
|
|
110
|
+
|
|
85
111
|
const inNoDataState = useMemo(() => {
|
|
86
112
|
const hasApplyBehavior = hasDashboardApplyBehavior(state.config.visualizations)
|
|
87
113
|
|
|
@@ -89,18 +115,18 @@ export default function CdcDashboard({
|
|
|
89
115
|
return true
|
|
90
116
|
}
|
|
91
117
|
|
|
92
|
-
|
|
93
|
-
const sharedFilters = state.config.dashboard?.sharedFilters || []
|
|
94
|
-
const hasResetFilters = sharedFilters.some(isFilterAtResetState)
|
|
95
|
-
if (hasResetFilters) {
|
|
118
|
+
if (hasIncompleteSharedFilters) {
|
|
96
119
|
return true
|
|
97
120
|
}
|
|
98
121
|
|
|
99
|
-
const vals = reloadURLHelpers
|
|
122
|
+
const vals = reloadURLHelpers
|
|
123
|
+
.getDatasetKeys(state.config, { includeDashboardConditionDatasetKeys: false })
|
|
124
|
+
.map(key => state.data[key])
|
|
100
125
|
|
|
101
126
|
// Check if there are any visualizations that actually need data
|
|
102
127
|
// Markup-includes without dataKey don't require dashboard data
|
|
103
128
|
const visualizationsNeedingData = Object.values(state.config.visualizations).filter(viz => {
|
|
129
|
+
if (viz.type === 'dashboardFilters') return false
|
|
104
130
|
return viz.type !== 'markup-include' || viz.dataKey
|
|
105
131
|
})
|
|
106
132
|
|
|
@@ -109,7 +135,15 @@ export default function CdcDashboard({
|
|
|
109
135
|
|
|
110
136
|
if (!vals.length) return true
|
|
111
137
|
return vals.some(val => val === undefined)
|
|
112
|
-
}, [
|
|
138
|
+
}, [
|
|
139
|
+
state.data,
|
|
140
|
+
state.config.visualizations,
|
|
141
|
+
state.config.datasets,
|
|
142
|
+
state.config.rows,
|
|
143
|
+
state.filtersApplied,
|
|
144
|
+
hasIncompleteSharedFilters,
|
|
145
|
+
hasFiltersIncompleteCondition
|
|
146
|
+
])
|
|
113
147
|
|
|
114
148
|
const vizRowColumnLocator = getVizRowColumnLocator(state.config.rows)
|
|
115
149
|
|
|
@@ -134,13 +168,9 @@ export default function CdcDashboard({
|
|
|
134
168
|
const filters = newFilters || config.dashboard.sharedFilters
|
|
135
169
|
const datasetKeys = reloadURLHelpers.getDatasetKeys(config)
|
|
136
170
|
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: emptyFilteredData })
|
|
141
|
-
|
|
142
|
-
const newData = {} // Start with empty object instead of cloning existing data
|
|
143
|
-
const newDatasets = config.datasets
|
|
171
|
+
setAPILoading(true)
|
|
172
|
+
const newData = {}
|
|
173
|
+
const newDatasets = { ...config.datasets }
|
|
144
174
|
let dataWasFetched = false
|
|
145
175
|
let newFileName = ''
|
|
146
176
|
|
|
@@ -207,7 +237,6 @@ export default function CdcDashboard({
|
|
|
207
237
|
newFileName
|
|
208
238
|
)
|
|
209
239
|
|
|
210
|
-
setAPILoading(true)
|
|
211
240
|
await fetchRemoteData(dataUrlFinal)
|
|
212
241
|
.then(({ data: fetchedData, dataMetadata }) => {
|
|
213
242
|
let data: any[] = fetchedData
|
|
@@ -220,9 +249,7 @@ export default function CdcDashboard({
|
|
|
220
249
|
console.error('Error standardizing data:', e)
|
|
221
250
|
}
|
|
222
251
|
}
|
|
223
|
-
newDatasets[datasetKey]
|
|
224
|
-
newDatasets[datasetKey].dataMetadata = dataMetadata
|
|
225
|
-
newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
|
|
252
|
+
newDatasets[datasetKey] = { ...newDatasets[datasetKey], data, dataMetadata, runtimeDataUrl: dataUrlFinal }
|
|
226
253
|
newData[datasetKey] = data
|
|
227
254
|
})
|
|
228
255
|
.catch(e => {
|
|
@@ -231,8 +258,7 @@ export default function CdcDashboard({
|
|
|
231
258
|
type: 'ADD_ERROR_MESSAGE',
|
|
232
259
|
payload: 'There was a problem returning data. Please try again.'
|
|
233
260
|
})
|
|
234
|
-
newDatasets[datasetKey]
|
|
235
|
-
newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
|
|
261
|
+
newDatasets[datasetKey] = { ...newDatasets[datasetKey], data: [], runtimeDataUrl: dataUrlFinal }
|
|
236
262
|
newData[datasetKey] = []
|
|
237
263
|
})
|
|
238
264
|
}
|
|
@@ -247,7 +273,10 @@ export default function CdcDashboard({
|
|
|
247
273
|
return acc
|
|
248
274
|
}, {})
|
|
249
275
|
const _newData = { ...newData, ...dataFiles }
|
|
250
|
-
dispatch({
|
|
276
|
+
dispatch({
|
|
277
|
+
type: 'SET_DATA',
|
|
278
|
+
payload: { data: _newData, activeDashboard: config.activeDashboard }
|
|
279
|
+
})
|
|
251
280
|
const dataFilterIndexes = config.dashboard.sharedFilters.reduce((acc, filter, index) => {
|
|
252
281
|
if (filter.type === 'datafilter') acc.push(index)
|
|
253
282
|
return acc
|
|
@@ -264,7 +293,10 @@ export default function CdcDashboard({
|
|
|
264
293
|
{},
|
|
265
294
|
_newData
|
|
266
295
|
)
|
|
267
|
-
dispatch({
|
|
296
|
+
dispatch({
|
|
297
|
+
type: 'SET_FILTERED_DATA',
|
|
298
|
+
payload: { filteredData, activeDashboard: config.activeDashboard }
|
|
299
|
+
})
|
|
268
300
|
const visualizations = reloadURLHelpers.getVisualizationsWithFormattedData(
|
|
269
301
|
config.visualizations as Record<string, Visualization>,
|
|
270
302
|
newData
|
|
@@ -308,11 +340,46 @@ export default function CdcDashboard({
|
|
|
308
340
|
|
|
309
341
|
const newFilteredData = getFilteredData({ ...state, config: newConfig }, filteredData)
|
|
310
342
|
|
|
311
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
343
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: { filteredData: newFilteredData } })
|
|
312
344
|
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
313
345
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: newConfig.dashboard.sharedFilters })
|
|
314
346
|
}
|
|
315
347
|
|
|
348
|
+
// Get the initial/reset value for a filter (captured at mount to avoid reading mutated state)
|
|
349
|
+
const getFilterInitialValue = (filter: SharedFilter): string => {
|
|
350
|
+
const key = filter.setBy || ''
|
|
351
|
+
return initialFilterValues[key] ?? filter.defaultValue ?? filter.values?.[0] ?? ''
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const clearSharedFilter = (key: string) => {
|
|
355
|
+
const { config: newConfig, filteredData } = cloneDeep(state)
|
|
356
|
+
|
|
357
|
+
for (let i = 0; i < newConfig.dashboard.sharedFilters.length; i++) {
|
|
358
|
+
const filter = newConfig.dashboard.sharedFilters[i]
|
|
359
|
+
if (filter.setBy === key) {
|
|
360
|
+
filter.active = getFilterInitialValue(filter)
|
|
361
|
+
break
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const newFilteredData = getFilteredData({ ...state, config: newConfig }, filteredData)
|
|
366
|
+
|
|
367
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: { filteredData: newFilteredData } })
|
|
368
|
+
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
369
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: newConfig.dashboard.sharedFilters })
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const hasActiveSharedFilter = (key: string): boolean => {
|
|
373
|
+
const filter = state.config.dashboard?.sharedFilters?.find(f => f.setBy === key)
|
|
374
|
+
if (!filter) return false
|
|
375
|
+
|
|
376
|
+
// Get the initial/default value for this filter
|
|
377
|
+
const initialValue = getFilterInitialValue(filter)
|
|
378
|
+
|
|
379
|
+
// Filter is "user-active" only if active differs from the initial value
|
|
380
|
+
return filter.active !== undefined && filter.active !== '' && filter.active !== initialValue
|
|
381
|
+
}
|
|
382
|
+
|
|
316
383
|
const setEventData = ({ detail }, data, filteredData) => {
|
|
317
384
|
try {
|
|
318
385
|
const newDatasets = Object.keys(detail).reduce((acc, key) => {
|
|
@@ -323,8 +390,8 @@ export default function CdcDashboard({
|
|
|
323
390
|
}, {})
|
|
324
391
|
const newConfig = { ...state, data: { ...data, ...newDatasets } }
|
|
325
392
|
const newFilteredData = getFilteredData(newConfig, cloneDeep(filteredData))
|
|
326
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
|
|
327
|
-
dispatch({ type: 'SET_DATA', payload: { ...data, ...newDatasets } })
|
|
393
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: { filteredData: newFilteredData } })
|
|
394
|
+
dispatch({ type: 'SET_DATA', payload: { data: { ...data, ...newDatasets } } })
|
|
328
395
|
} catch (e) {
|
|
329
396
|
console.error('Error setting event data: ', e)
|
|
330
397
|
}
|
|
@@ -343,24 +410,29 @@ export default function CdcDashboard({
|
|
|
343
410
|
const loadAllFilters = shouldLoadAllFilters(config, isEditor && !isPreview)
|
|
344
411
|
let sharedFiltersWithValues = addValuesToDashboardFilters(config.dashboard.sharedFilters, state.data)
|
|
345
412
|
sharedFiltersWithValues = updateChildFilters(sharedFiltersWithValues, state.data)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
413
|
+
const filterPromise = loadAPIFilters(sharedFiltersWithValues, apiFilterDropdowns, loadAllFilters)
|
|
414
|
+
if (!filterPromise) {
|
|
415
|
+
setAPILoading(false)
|
|
416
|
+
} else {
|
|
417
|
+
setAPILoading(true)
|
|
418
|
+
filterPromise.then(newFilters => {
|
|
419
|
+
const allValuesSelected = newFilters.every(filter => {
|
|
420
|
+
return filter.type === 'datafilter' || filter.active
|
|
421
|
+
})
|
|
422
|
+
if (allValuesSelected) {
|
|
423
|
+
reloadURLData(newFilters)
|
|
424
|
+
} else {
|
|
425
|
+
setAPILoading(false)
|
|
426
|
+
}
|
|
350
427
|
})
|
|
351
|
-
|
|
352
|
-
reloadURLData(newFilters)
|
|
353
|
-
} else {
|
|
354
|
-
setAPILoading(false)
|
|
355
|
-
}
|
|
356
|
-
})
|
|
428
|
+
}
|
|
357
429
|
}, [isEditor, isPreview, state.config?.activeDashboard])
|
|
358
430
|
|
|
359
431
|
useEffect(() => {
|
|
360
432
|
return () => {
|
|
361
433
|
// Clear all data when component unmounts to prevent memory leaks
|
|
362
|
-
dispatch({ type: 'SET_DATA', payload: {} })
|
|
363
|
-
dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
|
|
434
|
+
dispatch({ type: 'SET_DATA', payload: { data: {} } })
|
|
435
|
+
dispatch({ type: 'SET_FILTERED_DATA', payload: { filteredData: {} } })
|
|
364
436
|
|
|
365
437
|
// Clear any pending API requests
|
|
366
438
|
setAPILoading(false)
|
|
@@ -470,6 +542,8 @@ export default function CdcDashboard({
|
|
|
470
542
|
_updateConfig={_updateConfig}
|
|
471
543
|
isDebug={isDebug}
|
|
472
544
|
setSharedFilter={setSharedFilter}
|
|
545
|
+
clearSharedFilter={clearSharedFilter}
|
|
546
|
+
hasActiveSharedFilter={hasActiveSharedFilter}
|
|
473
547
|
apiFilterDropdowns={apiFilterDropdowns}
|
|
474
548
|
state={state}
|
|
475
549
|
interactionLabel={interactionLabel}
|
|
@@ -481,24 +555,30 @@ export default function CdcDashboard({
|
|
|
481
555
|
|
|
482
556
|
if (!subVisualizationEditing) {
|
|
483
557
|
body = (
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
<
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
558
|
+
<DashboardCopyPasteProvider>
|
|
559
|
+
<DndProvider backend={HTML5Backend}>
|
|
560
|
+
{apiLoading && <Loader fullScreen={true} />}
|
|
561
|
+
<div className='header-container'>
|
|
562
|
+
<Header />
|
|
563
|
+
<VisualizationsPanel />
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
<div className='layout-container'>
|
|
567
|
+
<Grid />
|
|
568
|
+
</div>
|
|
569
|
+
</DndProvider>
|
|
570
|
+
</DashboardCopyPasteProvider>
|
|
495
571
|
)
|
|
496
572
|
}
|
|
497
573
|
} else {
|
|
498
574
|
const { config } = state
|
|
499
575
|
const { title, description } = config.dashboard || {}
|
|
576
|
+
const hasDashboardDownloadButton = config.table?.downloadImageButton || config.table?.downloadPdfButton
|
|
500
577
|
|
|
501
|
-
const filteredRows =
|
|
578
|
+
const filteredRows =
|
|
579
|
+
config.rows
|
|
580
|
+
?.map((row, index) => ({ row, index }))
|
|
581
|
+
.filter(({ row }) => row.columns.some(col => getColumnWidgetEntries(col).length > 0)) || []
|
|
502
582
|
|
|
503
583
|
body = (
|
|
504
584
|
<>
|
|
@@ -525,49 +605,60 @@ export default function CdcDashboard({
|
|
|
525
605
|
{/* Description */}
|
|
526
606
|
{description && <div className='subtext cove-prose mb-4'>{parse(description)}</div>}
|
|
527
607
|
{/* Visualizations */}
|
|
528
|
-
{filteredRows
|
|
529
|
-
<VisualizationRow
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
608
|
+
{filteredRows.map(({ row, index }, renderIndex) => (
|
|
609
|
+
<ErrorBoundary key={`row__${index}`} component={`VisualizationRow-${index}`}>
|
|
610
|
+
<VisualizationRow
|
|
611
|
+
allExpanded={allExpanded}
|
|
612
|
+
groupName={''}
|
|
613
|
+
row={row}
|
|
614
|
+
rowIndex={index}
|
|
615
|
+
setSharedFilter={setSharedFilter}
|
|
616
|
+
clearSharedFilter={clearSharedFilter}
|
|
617
|
+
hasActiveSharedFilter={hasActiveSharedFilter}
|
|
618
|
+
setAllExpanded={setAllExpanded}
|
|
619
|
+
updateChildConfig={updateChildConfig}
|
|
620
|
+
apiFilterDropdowns={apiFilterDropdowns}
|
|
621
|
+
currentViewport={currentViewport}
|
|
622
|
+
inNoDataState={inNoDataState}
|
|
623
|
+
interactionLabel={interactionLabel}
|
|
624
|
+
isLastRow={renderIndex === filteredRows.length - 1}
|
|
625
|
+
/>
|
|
626
|
+
</ErrorBoundary>
|
|
544
627
|
))}
|
|
545
628
|
|
|
546
|
-
{inNoDataState
|
|
629
|
+
{inNoDataState && !(hasIncompleteSharedFilters && hasFiltersIncompleteCondition) ? (
|
|
630
|
+
<div className='mt-5'>Please complete your selection to continue.</div>
|
|
631
|
+
) : (
|
|
632
|
+
<></>
|
|
633
|
+
)}
|
|
547
634
|
|
|
548
635
|
{/* Image or PDF Inserts */}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
636
|
+
{hasDashboardDownloadButton && (
|
|
637
|
+
<section className='download-buttons'>
|
|
638
|
+
{config.table?.downloadImageButton && (
|
|
639
|
+
<MediaControls.Button
|
|
640
|
+
title='Download Dashboard as Image'
|
|
641
|
+
type='image'
|
|
642
|
+
state={config}
|
|
643
|
+
text='Download Dashboard Image'
|
|
644
|
+
elementToCapture={imageId}
|
|
645
|
+
interactionLabel={interactionLabel}
|
|
646
|
+
appearance={config.table?.downloadImageButtonStyle === 'link' ? 'link' : 'button'}
|
|
647
|
+
/>
|
|
648
|
+
)}
|
|
649
|
+
{config.table?.downloadPdfButton && (
|
|
650
|
+
<MediaControls.Button
|
|
651
|
+
title='Download Dashboard as PDF'
|
|
652
|
+
type='pdf'
|
|
653
|
+
state={config}
|
|
654
|
+
text='Download Dashboard PDF'
|
|
655
|
+
elementToCapture={imageId}
|
|
656
|
+
interactionLabel={interactionLabel}
|
|
657
|
+
appearance={config.table?.downloadImageButtonStyle === 'link' ? 'link' : 'button'}
|
|
658
|
+
/>
|
|
659
|
+
)}
|
|
660
|
+
</section>
|
|
661
|
+
)}
|
|
571
662
|
|
|
572
663
|
{/* Data Table */}
|
|
573
664
|
{config.table?.show && config.data && (
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { DashboardCopyPasteContext, DashboardCopyPasteProvider } from './DashboardCopyPasteContext'
|
|
5
|
+
|
|
6
|
+
const CopyPasteHarness = () => {
|
|
7
|
+
const { copiedWidget, copyWidget } = useContext(DashboardCopyPasteContext)
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<button onClick={() => copyWidget({ sourceWidgetKey: 'source-widget', label: 'Source Component' })}>
|
|
12
|
+
Copy Test Component
|
|
13
|
+
</button>
|
|
14
|
+
{copiedWidget && <span>{copiedWidget.label}</span>}
|
|
15
|
+
</>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('DashboardCopyPasteProvider', () => {
|
|
20
|
+
it('clears copy mode with Escape', () => {
|
|
21
|
+
render(
|
|
22
|
+
<DashboardCopyPasteProvider>
|
|
23
|
+
<CopyPasteHarness />
|
|
24
|
+
</DashboardCopyPasteProvider>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
fireEvent.click(screen.getByText('Copy Test Component'))
|
|
28
|
+
expect(screen.getByText('Source Component')).toBeInTheDocument()
|
|
29
|
+
|
|
30
|
+
fireEvent.keyDown(window, { key: 'Escape' })
|
|
31
|
+
expect(screen.queryByText('Source Component')).not.toBeInTheDocument()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export type CopiedDashboardWidget = {
|
|
4
|
+
sourceWidgetKey: string
|
|
5
|
+
label: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type DashboardCopyPasteContextValue = {
|
|
9
|
+
copiedWidget?: CopiedDashboardWidget
|
|
10
|
+
copyWidget: (widget: CopiedDashboardWidget) => void
|
|
11
|
+
clearCopiedWidget: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const initialContext: DashboardCopyPasteContextValue = {
|
|
15
|
+
copyWidget: () => {},
|
|
16
|
+
clearCopiedWidget: () => {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DashboardCopyPasteContext = createContext<DashboardCopyPasteContextValue>(initialContext)
|
|
20
|
+
|
|
21
|
+
export const DashboardCopyPasteProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
22
|
+
const [copiedWidget, setCopiedWidget] = useState<CopiedDashboardWidget | undefined>()
|
|
23
|
+
const clearCopiedWidget = useCallback(() => setCopiedWidget(undefined), [])
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!copiedWidget) return
|
|
27
|
+
|
|
28
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
29
|
+
if (event.key === 'Escape') {
|
|
30
|
+
clearCopiedWidget()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
35
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
36
|
+
}, [clearCopiedWidget, copiedWidget])
|
|
37
|
+
|
|
38
|
+
const value = useMemo(
|
|
39
|
+
() => ({
|
|
40
|
+
copiedWidget,
|
|
41
|
+
copyWidget: setCopiedWidget,
|
|
42
|
+
clearCopiedWidget
|
|
43
|
+
}),
|
|
44
|
+
[clearCopiedWidget, copiedWidget]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return <DashboardCopyPasteContext.Provider value={value}>{children}</DashboardCopyPasteContext.Provider>
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { expect, userEvent, within } from 'storybook/test'
|
|
3
|
+
import { performAndAssert, waitForPresence } from '@cdc/core/helpers/testing'
|
|
4
|
+
import CdcEditor from '@cdc/editor/src/CdcEditor'
|
|
5
|
+
import SharedFilterRowDeleteConfig from '../../examples/dashboard-shared-filter-row-delete-cleanup.json'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof CdcEditor> = {
|
|
8
|
+
title: 'Components/Pages/Dashboard/Regression Editor',
|
|
9
|
+
component: CdcEditor
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default meta
|
|
13
|
+
type Story = StoryObj<typeof CdcEditor>
|
|
14
|
+
|
|
15
|
+
export const Delete_Rows_With_Stale_Shared_Filter_Targets: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
config: SharedFilterRowDeleteConfig
|
|
18
|
+
},
|
|
19
|
+
play: async ({ canvasElement }) => {
|
|
20
|
+
const canvas = within(canvasElement)
|
|
21
|
+
const user = userEvent.setup()
|
|
22
|
+
|
|
23
|
+
await waitForPresence('.builder-row', canvasElement)
|
|
24
|
+
|
|
25
|
+
const getState = () => ({
|
|
26
|
+
rowCount: canvasElement.querySelectorAll('.builder-row').length,
|
|
27
|
+
deleteButtonCount: canvasElement.querySelectorAll('[title="Delete Row"]').length,
|
|
28
|
+
hasFiltersRow: (canvasElement.textContent || '').includes('dashboard-filters'),
|
|
29
|
+
hasRowA: (canvasElement.textContent || '').includes('Row A'),
|
|
30
|
+
hasRowB: (canvasElement.textContent || '').includes('Row B'),
|
|
31
|
+
hasRowC: (canvasElement.textContent || '').includes('Row C'),
|
|
32
|
+
noDataVisible: (canvasElement.textContent || '').includes('No Data Available'),
|
|
33
|
+
crashTextVisible: (canvasElement.textContent || '').includes('Cannot read properties of undefined')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const initialState = getState()
|
|
37
|
+
expect(initialState.rowCount).toBe(4)
|
|
38
|
+
expect(initialState.deleteButtonCount).toBe(4)
|
|
39
|
+
expect(initialState.hasRowA).toBe(true)
|
|
40
|
+
expect(initialState.hasRowB).toBe(true)
|
|
41
|
+
expect(initialState.hasRowC).toBe(true)
|
|
42
|
+
expect(initialState.noDataVisible).toBe(false)
|
|
43
|
+
expect(initialState.crashTextVisible).toBe(false)
|
|
44
|
+
|
|
45
|
+
await performAndAssert(
|
|
46
|
+
'Delete one shared-filter row without crashing',
|
|
47
|
+
getState,
|
|
48
|
+
async () => await user.click(canvas.getAllByTitle('Delete Row')[1]),
|
|
49
|
+
(_before, after) =>
|
|
50
|
+
after.rowCount === 3 &&
|
|
51
|
+
after.deleteButtonCount === 3 &&
|
|
52
|
+
!after.hasRowA &&
|
|
53
|
+
after.hasRowB &&
|
|
54
|
+
after.hasRowC &&
|
|
55
|
+
!after.noDataVisible &&
|
|
56
|
+
!after.crashTextVisible
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
await performAndAssert(
|
|
60
|
+
'Delete another shared-filter row after indices shift',
|
|
61
|
+
getState,
|
|
62
|
+
async () => await user.click(canvas.getAllByTitle('Delete Row')[2]),
|
|
63
|
+
(_before, after) =>
|
|
64
|
+
after.rowCount === 2 &&
|
|
65
|
+
after.deleteButtonCount === 2 &&
|
|
66
|
+
after.hasRowB &&
|
|
67
|
+
!after.hasRowC &&
|
|
68
|
+
!after.noDataVisible &&
|
|
69
|
+
!after.crashTextVisible
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|