@cdc/dashboard 4.25.10 → 4.26.1
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/Dynamic_Data.md +66 -0
- package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
- package/dist/cdcdashboard.js +84214 -79641
- package/examples/api-dashboard-data.json +272 -0
- package/examples/api-dashboard-years.json +11 -0
- package/examples/api-geographies-data.json +11 -0
- package/examples/api-test/categories.json +18 -0
- package/examples/api-test/chart-data.json +602 -0
- package/examples/api-test/topics.json +47 -0
- package/examples/api-test/years.json +22 -0
- package/examples/markup-axis-label.json +4167 -0
- package/examples/private/big-dashboard.json +39095 -39077
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/chronic-dash.json +1584 -0
- package/examples/private/clade-2.json +430 -0
- package/examples/private/diabetes.json +546 -196
- package/examples/private/map-issue.json +2260 -0
- package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
- package/examples/private/mpinc-state-reports.json +2260 -0
- package/examples/private/mpox.json +38128 -0
- package/examples/private/nwss/rsv.json +1240 -0
- package/examples/private/reset.json +32920 -0
- package/examples/private/simple-dash.json +490 -0
- package/examples/private/test-dash.json +0 -0
- package/examples/private/test123.json +491 -0
- package/examples/test-api-filter-reset.json +132 -0
- package/examples/test-dashboard-simple.json +503 -0
- package/index.html +25 -26
- package/package.json +11 -11
- package/src/CdcDashboardComponent.tsx +35 -10
- package/src/DashboardContext.tsx +3 -1
- package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
- package/src/_stories/Dashboard.stories.tsx +402 -1
- package/src/_stories/_mock/custom-order-new-values.json +116 -0
- package/src/_stories/_mock/filter-cascade.json +3350 -0
- package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
- package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
- package/src/_stories/_mock/parent-child-filters.json +233 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +54 -31
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +118 -50
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +96 -108
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +196 -59
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +129 -29
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +62 -3
- package/src/components/DataDesignerModal.tsx +18 -6
- package/src/components/Header/Header.tsx +53 -21
- package/src/components/Toggle/Toggle.tsx +48 -48
- package/src/components/VisualizationRow.tsx +73 -6
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
- package/src/components/Widget/Widget.tsx +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +24 -6
- package/src/helpers/apiFilterHelpers.ts +26 -2
- package/src/helpers/changeFilterActive.ts +67 -65
- package/src/helpers/filterData.ts +52 -7
- package/src/helpers/filterResetHelpers.ts +102 -0
- package/src/helpers/formatConfigBeforeSave.ts +6 -5
- package/src/helpers/getUpdateConfig.ts +91 -91
- package/src/helpers/getVizConfig.ts +2 -2
- package/src/helpers/loadAPIFilters.ts +109 -99
- package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
- package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
- package/src/helpers/updateChildFilters.ts +50 -27
- package/src/index.tsx +1 -0
- package/src/scss/editor-panel.scss +3 -431
- package/src/scss/main.scss +142 -25
- package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
- package/src/test/CdcDashboard.test.jsx +9 -4
- package/src/types/Dashboard.ts +1 -0
- package/src/types/DashboardFilters.ts +9 -8
- package/src/types/FilterStyles.ts +8 -7
- package/src/types/SharedFilter.ts +13 -0
- package/LICENSE +0 -201
- package/examples/private/DEV-11072.json +0 -7591
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_qaly_data.csv +0 -18721
- package/examples/private/burden_toolkit_mortality_yll_data.csv +0 -18721
- package/examples/private/pedro.json +0 -1
- package/src/helpers/getAutoLoadVisualization.ts +0 -11
- package/src/scss/mixins.scss +0 -47
- package/src/scss/variables.scss +0 -5
- /package/dist/{cdcdashboard-548642e6.es.js → cdcdashboard-Ct2SB0vL.es.js} +0 -0
|
@@ -4,14 +4,13 @@ import { useContext, useMemo, useState } from 'react'
|
|
|
4
4
|
import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
|
|
5
5
|
import Modal from '@cdc/core/components/ui/Modal'
|
|
6
6
|
import Loader from '@cdc/core/components/Loader'
|
|
7
|
-
import { CheckBox } from '@cdc/core/components/EditorPanel/Inputs'
|
|
7
|
+
import { CheckBox, Select } from '@cdc/core/components/EditorPanel/Inputs'
|
|
8
8
|
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
9
9
|
import _ from 'lodash'
|
|
10
10
|
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
11
11
|
import DataTransform from '@cdc/core/helpers/DataTransform'
|
|
12
12
|
import { ConfigureData } from '@cdc/core/types/ConfigureData'
|
|
13
13
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
14
|
-
import InputSelect from '@cdc/core/components/inputs/InputSelect'
|
|
15
14
|
|
|
16
15
|
type DataDesignerModalProps = {
|
|
17
16
|
rowIndex: number
|
|
@@ -134,10 +133,18 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
|
|
|
134
133
|
{loadingAPIData && <Loader fullScreen />}
|
|
135
134
|
<div className='dataset-selector-container'>
|
|
136
135
|
Select a dataset:
|
|
137
|
-
<select
|
|
136
|
+
<select
|
|
137
|
+
className='dataset-selector cove-form-select'
|
|
138
|
+
value={configureData.dataKey || ''}
|
|
139
|
+
onChange={changeDataset}
|
|
140
|
+
>
|
|
138
141
|
<option value=''>Select a dataset</option>
|
|
139
142
|
{config.datasets &&
|
|
140
|
-
Object.keys(config.datasets).map(datasetKey =>
|
|
143
|
+
Object.keys(config.datasets).map(datasetKey => (
|
|
144
|
+
<option key={datasetKey} value={datasetKey}>
|
|
145
|
+
{datasetKey}
|
|
146
|
+
</option>
|
|
147
|
+
))}
|
|
141
148
|
</select>
|
|
142
149
|
{vizKey && (
|
|
143
150
|
// only shows for visualizations
|
|
@@ -186,8 +193,13 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
|
|
|
186
193
|
/>
|
|
187
194
|
) : (
|
|
188
195
|
<>
|
|
189
|
-
<
|
|
190
|
-
options={Object.keys(
|
|
196
|
+
<Select
|
|
197
|
+
options={Object.keys(
|
|
198
|
+
config.rows[rowIndex]?.data?.[0] ||
|
|
199
|
+
configureData.data?.[0] ||
|
|
200
|
+
config.datasets[configureData.dataKey]?.data?.[0] ||
|
|
201
|
+
{}
|
|
202
|
+
)}
|
|
191
203
|
value={config.rows[rowIndex].multiVizColumn}
|
|
192
204
|
label='Multi-Visualization Column'
|
|
193
205
|
initial='--Select--'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useContext, useRef, useEffect } from 'react'
|
|
2
2
|
import cloneConfig from '@cdc/core/helpers/cloneConfig'
|
|
3
3
|
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
4
4
|
|
|
@@ -20,8 +20,22 @@ const Header = (props: HeaderProps) => {
|
|
|
20
20
|
const dispatch = useContext(DashboardDispatchContext)
|
|
21
21
|
const back = () => {
|
|
22
22
|
if (!visualizationKey) return
|
|
23
|
+
|
|
23
24
|
const newConfig = cloneConfig(config)
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
// Ensure visualizations object exists
|
|
27
|
+
if (!newConfig.visualizations || !newConfig.visualizations[visualizationKey]) {
|
|
28
|
+
console.error(`Visualization ${visualizationKey} not found in config`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Explicitly set editing to false
|
|
33
|
+
newConfig.visualizations[visualizationKey] = {
|
|
34
|
+
...newConfig.visualizations[visualizationKey],
|
|
35
|
+
editing: false,
|
|
36
|
+
showEditorPanel: false
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
dispatch({ type: 'SET_CONFIG', payload: newConfig })
|
|
26
40
|
|
|
27
41
|
// the Widget component will do a data fetch if no data is available for the visualization
|
|
@@ -56,21 +70,24 @@ const Header = (props: HeaderProps) => {
|
|
|
56
70
|
return strippedState
|
|
57
71
|
}
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
const parsedData = convertStateToConfig()
|
|
61
|
-
|
|
62
|
-
// Emit the data in a regular JS event so it can be consumed by anything.
|
|
63
|
-
const event = new CustomEvent('updateVizConfig', { detail: JSON.stringify(parsedData) })
|
|
64
|
-
|
|
65
|
-
window.dispatchEvent(event)
|
|
73
|
+
const configStringRef = useRef<string>()
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
// Only update parent when config content actually changes (not just reference)
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const configString = JSON.stringify(convertStateToConfig())
|
|
78
|
+
if (configStringRef.current !== configString) {
|
|
79
|
+
configStringRef.current = configString
|
|
80
|
+
|
|
81
|
+
// Emit the data in a regular JS event so it can be consumed by anything.
|
|
82
|
+
const event = new CustomEvent('updateVizConfig', { detail: configString })
|
|
83
|
+
window.dispatchEvent(event)
|
|
84
|
+
|
|
85
|
+
// Pass up to Editor if needed
|
|
86
|
+
if (setParentConfig) {
|
|
87
|
+
setParentConfig(JSON.parse(configString))
|
|
88
|
+
}
|
|
70
89
|
}
|
|
71
|
-
|
|
72
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
-
}, [config])
|
|
90
|
+
}, [config, setParentConfig])
|
|
74
91
|
|
|
75
92
|
const handleCheck = e => {
|
|
76
93
|
const { checked } = e.currentTarget
|
|
@@ -96,12 +113,27 @@ const Header = (props: HeaderProps) => {
|
|
|
96
113
|
multidashboard
|
|
97
114
|
</span>
|
|
98
115
|
<br />
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
116
|
+
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '10px' }}>
|
|
117
|
+
<input
|
|
118
|
+
type='text'
|
|
119
|
+
placeholder='Enter Dashboard Name Here'
|
|
120
|
+
defaultValue={config.dashboard?.title}
|
|
121
|
+
onChange={e => changeConfigValue('dashboard', 'title', e.target.value)}
|
|
122
|
+
style={{ flex: 1 }}
|
|
123
|
+
/>
|
|
124
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: '3px', fontSize: '0.85em' }}>
|
|
125
|
+
<span style={{ fontSize: '0.8em' }}>Title Style</span>
|
|
126
|
+
<select
|
|
127
|
+
value={config.dashboard.titleStyle}
|
|
128
|
+
onChange={e => changeConfigValue('dashboard', 'titleStyle', e.target.value)}
|
|
129
|
+
style={{ fontSize: '0.9em' }}
|
|
130
|
+
>
|
|
131
|
+
<option value='small'>Small</option>
|
|
132
|
+
<option value='large'>Large</option>
|
|
133
|
+
<option value='legacy'>Legacy</option>
|
|
134
|
+
</select>
|
|
135
|
+
</label>
|
|
136
|
+
</div>
|
|
105
137
|
</div>
|
|
106
138
|
)}
|
|
107
139
|
{!subEditor && (
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { ConfigRow } from '../../types/ConfigRow'
|
|
2
|
-
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
3
|
-
import { getIcon } from '../../helpers/iconHash'
|
|
4
|
-
import { labelHash } from '@cdc/core/helpers/labelHash'
|
|
5
|
-
import './toggle-style.css'
|
|
6
|
-
import _ from 'lodash'
|
|
7
|
-
|
|
8
|
-
type ToggleProps = {
|
|
9
|
-
active: number
|
|
10
|
-
row: ConfigRow
|
|
11
|
-
visualizations: Record<string, AnyVisualization>
|
|
12
|
-
setToggled: (colIndex: number) => void
|
|
13
|
-
}
|
|
14
|
-
const Toggle: React.FC<ToggleProps> = ({ active, row, visualizations, setToggled, text }) => {
|
|
15
|
-
const selectItem = (colIndex, e = null) => {
|
|
16
|
-
if (e?.key && e.key !== 'Enter' && e.key !== ' ') return
|
|
17
|
-
if (e?.key === ' ') e.preventDefault() // Prevent page scroll
|
|
18
|
-
setToggled(colIndex)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<div className='toggle-component' role='radiogroup' aria-label='Visualization options'>
|
|
23
|
-
{row.columns.map((col, colIndex) => {
|
|
24
|
-
if (!col.widget) return null
|
|
25
|
-
const type = visualizations[col.widget].type
|
|
26
|
-
// Get the column toggele Text or default to the type
|
|
27
|
-
const text = col.toggleName ? col.toggleName : labelHash[type]
|
|
28
|
-
const selected = colIndex === active
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
role='radio'
|
|
32
|
-
className={selected ? 'selected' : ''}
|
|
33
|
-
key={colIndex}
|
|
34
|
-
onClick={() => selectItem(colIndex)}
|
|
35
|
-
onKeyUp={e => selectItem(colIndex, e)}
|
|
36
|
-
aria-checked={selected}
|
|
37
|
-
tabIndex={0}
|
|
38
|
-
aria-label={`Toggle ${
|
|
39
|
-
>
|
|
40
|
-
<span aria-hidden='true'>{getIcon(visualizations[col.widget])}</span> <span>{text}</span>
|
|
41
|
-
</div>
|
|
42
|
-
)
|
|
43
|
-
})}
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export default Toggle
|
|
1
|
+
import { ConfigRow } from '../../types/ConfigRow'
|
|
2
|
+
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
import { getIcon } from '../../helpers/iconHash'
|
|
4
|
+
import { labelHash } from '@cdc/core/helpers/labelHash'
|
|
5
|
+
import './toggle-style.css'
|
|
6
|
+
import _ from 'lodash'
|
|
7
|
+
|
|
8
|
+
type ToggleProps = {
|
|
9
|
+
active: number
|
|
10
|
+
row: ConfigRow
|
|
11
|
+
visualizations: Record<string, AnyVisualization>
|
|
12
|
+
setToggled: (colIndex: number) => void
|
|
13
|
+
}
|
|
14
|
+
const Toggle: React.FC<ToggleProps> = ({ active, row, visualizations, setToggled, text }) => {
|
|
15
|
+
const selectItem = (colIndex, e = null) => {
|
|
16
|
+
if (e?.key && e.key !== 'Enter' && e.key !== ' ') return
|
|
17
|
+
if (e?.key === ' ') e.preventDefault() // Prevent page scroll
|
|
18
|
+
setToggled(colIndex)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className='toggle-component' role='radiogroup' aria-label='Visualization options'>
|
|
23
|
+
{row.columns.map((col, colIndex) => {
|
|
24
|
+
if (!col.widget) return null
|
|
25
|
+
const type = visualizations[col.widget].type
|
|
26
|
+
// Get the column toggele Text or default to the type
|
|
27
|
+
const text = col.toggleName ? col.toggleName : labelHash[type]
|
|
28
|
+
const selected = colIndex === active
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
role='radio'
|
|
32
|
+
className={selected ? 'selected' : ''}
|
|
33
|
+
key={colIndex}
|
|
34
|
+
onClick={() => selectItem(colIndex)}
|
|
35
|
+
onKeyUp={e => selectItem(colIndex, e)}
|
|
36
|
+
aria-checked={selected}
|
|
37
|
+
tabIndex={0}
|
|
38
|
+
aria-label={`Toggle ${text}`}
|
|
39
|
+
>
|
|
40
|
+
<span aria-hidden='true'>{getIcon(visualizations[col.widget])}</span> <span>{text}</span>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
})}
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default Toggle
|
|
@@ -93,6 +93,63 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
93
93
|
if (row.toggle) setToggled(0)
|
|
94
94
|
}, [config.activeDashboard, index])
|
|
95
95
|
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
// Trigger window resize event when tab changes to force chart re-render
|
|
98
|
+
if (row.toggle && toggledRow !== undefined) {
|
|
99
|
+
// Use setTimeout to ensure the d-none class has been removed first
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
window.dispatchEvent(new Event('resize'))
|
|
102
|
+
}, 50)
|
|
103
|
+
}
|
|
104
|
+
}, [toggledRow, row.toggle])
|
|
105
|
+
|
|
106
|
+
// Equalize TP5 data bite title heights in the same row
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const rowElement = document.querySelector(`[data-row-index="${index}"]`)
|
|
109
|
+
if (!rowElement) return
|
|
110
|
+
|
|
111
|
+
const tp5Titles = Array.from(rowElement.querySelectorAll('.bite__style--tp5 .cdc-callout__heading'))
|
|
112
|
+
if (tp5Titles.length <= 1) return // No need to equalize if there's only one or none
|
|
113
|
+
|
|
114
|
+
const equalizeTP5Titles = () => {
|
|
115
|
+
// Reset heights first
|
|
116
|
+
tp5Titles.forEach((title: HTMLElement) => {
|
|
117
|
+
title.style.minHeight = ''
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Calculate max height after reset
|
|
121
|
+
let maxHeight = 0
|
|
122
|
+
tp5Titles.forEach((title: HTMLElement) => {
|
|
123
|
+
const height = title.offsetHeight
|
|
124
|
+
if (height > maxHeight) maxHeight = height
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Apply max height to all titles
|
|
128
|
+
if (maxHeight > 0) {
|
|
129
|
+
tp5Titles.forEach((title: HTMLElement) => {
|
|
130
|
+
title.style.minHeight = `${maxHeight}px`
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Initial equalization
|
|
136
|
+
equalizeTP5Titles()
|
|
137
|
+
|
|
138
|
+
// Use ResizeObserver to watch for size changes in any of the titles
|
|
139
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
140
|
+
equalizeTP5Titles()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Observe all titles
|
|
144
|
+
tp5Titles.forEach(title => {
|
|
145
|
+
resizeObserver.observe(title as Element)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
resizeObserver.disconnect()
|
|
150
|
+
}
|
|
151
|
+
}, [index, row, config, filteredDataOverride])
|
|
152
|
+
|
|
96
153
|
const show = useMemo(() => {
|
|
97
154
|
if (row.toggle) {
|
|
98
155
|
return row.columns.map((col, i) => i === toggledRow)
|
|
@@ -166,13 +223,18 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
166
223
|
}
|
|
167
224
|
|
|
168
225
|
return (
|
|
169
|
-
<div
|
|
226
|
+
<div
|
|
227
|
+
className={`row${row.equalHeight ? ' equal-height' : ''}${row.toggle ? ' toggle' : ''}`}
|
|
228
|
+
key={`row__${index}`}
|
|
229
|
+
data-row-index={index}
|
|
230
|
+
>
|
|
170
231
|
{row.toggle && !inNoDataState && (
|
|
171
232
|
<Toggle row={row} visualizations={config.visualizations} active={toggledRow} setToggled={setToggled} />
|
|
172
233
|
)}
|
|
173
234
|
{row.columns.map((col, colIndex) => {
|
|
174
235
|
if (col.width) {
|
|
175
|
-
if (!col.widget)
|
|
236
|
+
if (!col.widget)
|
|
237
|
+
return <div key={`row__${index}__col__${colIndex}`} className={`col-12 col-md-${col.width}`}></div>
|
|
176
238
|
|
|
177
239
|
const visualizationConfig = getVizConfig(
|
|
178
240
|
col.widget,
|
|
@@ -212,9 +274,14 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
212
274
|
</a>
|
|
213
275
|
)
|
|
214
276
|
|
|
277
|
+
// Markup-includes with external URLs don't depend on dashboard data
|
|
278
|
+
const isMarkupIncludeWithoutDataDependency =
|
|
279
|
+
type === 'markup-include' && !visualizationConfig.dataKey && !visualizationConfig.data?.length
|
|
280
|
+
|
|
215
281
|
const hideVisualization =
|
|
216
282
|
inNoDataState &&
|
|
217
283
|
filterBehavior !== 'Apply Button' &&
|
|
284
|
+
!isMarkupIncludeWithoutDataDependency &&
|
|
218
285
|
(type !== 'dashboardFilters' || applyButtonNotClicked(visualizationConfig))
|
|
219
286
|
|
|
220
287
|
const shouldShow = row.toggle === undefined || (row.toggle && show[colIndex])
|
|
@@ -223,11 +290,11 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
223
290
|
type === 'dashboardFilters' &&
|
|
224
291
|
sharedFilterIndexes &&
|
|
225
292
|
sharedFilterIndexes.filter(idx => config.dashboard.sharedFilters?.[idx]?.showDropdown === false).length ===
|
|
226
|
-
|
|
227
|
-
const hasMarginBottom = !isLastRow && !hiddenDashboardFilters
|
|
293
|
+
sharedFilterIndexes.length
|
|
228
294
|
|
|
229
|
-
const vizWrapperClass = `col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${
|
|
230
|
-
|
|
295
|
+
const vizWrapperClass = `col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${
|
|
296
|
+
hideVisualization ? ' hide-parent-visualization' : ''
|
|
297
|
+
}${hiddenDashboardFilters ? ' hidden-dashboard-filters' : ''}`
|
|
231
298
|
const link =
|
|
232
299
|
config.table && config.table.show && config.datasets && table && table.showDataTableLink
|
|
233
300
|
? tableLink
|
|
@@ -51,7 +51,6 @@ const addVisualization = (type, subType) => {
|
|
|
51
51
|
markupVariables: [],
|
|
52
52
|
showHeader: true,
|
|
53
53
|
srcUrl: '#example',
|
|
54
|
-
title: 'Markup Include',
|
|
55
54
|
useInlineHTML: true
|
|
56
55
|
}
|
|
57
56
|
newVisualizationConfig.theme = 'theme-blue'
|
|
@@ -81,7 +80,7 @@ const addVisualization = (type, subType) => {
|
|
|
81
80
|
|
|
82
81
|
const VisualizationsPanel = () => {
|
|
83
82
|
const [advancedEditing, setAdvancedEditing] = useState(false)
|
|
84
|
-
const { config } = useContext(DashboardContext)
|
|
83
|
+
const { config, isEditor } = useContext(DashboardContext)
|
|
85
84
|
const dispatch = useContext(DashboardDispatchContext)
|
|
86
85
|
const loadConfig = incomingConfig => {
|
|
87
86
|
const newConfig = !incomingConfig.multiDashboards
|
|
@@ -124,7 +123,7 @@ const VisualizationsPanel = () => {
|
|
|
124
123
|
loadConfig={loadConfig}
|
|
125
124
|
config={config}
|
|
126
125
|
convertStateToConfig={() => undefined}
|
|
127
|
-
stripConfig={stripConfig}
|
|
126
|
+
stripConfig={cfg => stripConfig(cfg, isEditor)}
|
|
128
127
|
onExpandCollapse={() => {
|
|
129
128
|
setAdvancedEditing(!advancedEditing)
|
|
130
129
|
}}
|
|
@@ -123,7 +123,7 @@ const Widget = ({
|
|
|
123
123
|
if (!widgetConfig) return
|
|
124
124
|
dispatch({
|
|
125
125
|
type: 'UPDATE_VISUALIZATION',
|
|
126
|
-
payload: { vizKey: widgetConfig.uid as string, configureData: { editing: true } }
|
|
126
|
+
payload: { vizKey: widgetConfig.uid as string, configureData: { editing: true, showEditorPanel: true } }
|
|
127
127
|
})
|
|
128
128
|
loadSampleData()
|
|
129
129
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
-
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
2
|
+
import { getQueryStringFilterValue, isFilterHiddenByQuery } from '@cdc/core/helpers/queryStringUtils'
|
|
3
3
|
import { SharedFilter } from '../types/SharedFilter'
|
|
4
4
|
import { handleSorting } from '@cdc/core/components/Filters'
|
|
5
|
+
import { mergeCustomOrderValues } from '@cdc/core/helpers/mergeCustomOrderValues'
|
|
5
6
|
|
|
6
7
|
// Gets filter values from dataset
|
|
7
8
|
const generateValuesForFilter = (columnName: string, data: Record<string, any[]>) => {
|
|
@@ -34,13 +35,20 @@ export const addValuesToDashboardFilters = (
|
|
|
34
35
|
data: Record<string, any[]>,
|
|
35
36
|
filtersToSkip: number[] = []
|
|
36
37
|
): Array<SharedFilter> => {
|
|
37
|
-
|
|
38
|
+
const result = filters?.map((filter, index) => {
|
|
38
39
|
if (filtersToSkip.includes(index)) return filter
|
|
39
40
|
if (filter.type === 'urlfilter') return filter
|
|
40
41
|
const filterCopy = _.cloneDeep(filter)
|
|
41
|
-
|
|
42
|
+
|
|
43
|
+
// Only generate values from data if not pre-configured
|
|
44
|
+
const hasPreConfiguredValues = filter.values && filter.values.length > 0
|
|
45
|
+
const filterValues = hasPreConfiguredValues ? filter.values : generateValuesForFilter(getSelector(filter), data)
|
|
46
|
+
|
|
42
47
|
filterCopy.values = filterValues
|
|
43
48
|
|
|
49
|
+
// Merge new values with existing custom order (fixes DEV-11740 & DEV-11376)
|
|
50
|
+
filterCopy.orderedValues = mergeCustomOrderValues(filterValues, filterCopy.orderedValues, filterCopy.order)
|
|
51
|
+
|
|
44
52
|
if (filterValues.length > 0) {
|
|
45
53
|
const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
|
|
46
54
|
if (queryStringFilterValue) {
|
|
@@ -50,12 +58,21 @@ export const addValuesToDashboardFilters = (
|
|
|
50
58
|
const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
|
|
51
59
|
filterCopy.active = active.filter(val => defaultValues.includes(val))
|
|
52
60
|
} else {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
// Initialize active from defaultValue if not already set
|
|
62
|
+
// OR if defaultValue exists, always use it (overrides stale active from saved config)
|
|
63
|
+
if (filterCopy.defaultValue) {
|
|
64
|
+
filterCopy.active = filterCopy.defaultValue
|
|
65
|
+
} else if (!filterCopy.active) {
|
|
66
|
+
filterCopy.active = filterCopy.resetLabel || filterCopy.values[0]
|
|
67
|
+
}
|
|
56
68
|
}
|
|
57
69
|
}
|
|
58
70
|
|
|
71
|
+
// Check if filter should be hidden by query parameter
|
|
72
|
+
if (isFilterHiddenByQuery(filterCopy)) {
|
|
73
|
+
filterCopy.showDropdown = false
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
// Handle nested dropdown subGrouping.active property
|
|
60
77
|
if (filterCopy.subGrouping && filterCopy.subGrouping.valuesLookup) {
|
|
61
78
|
const groupName = filterCopy.active as string
|
|
@@ -83,4 +100,5 @@ export const addValuesToDashboardFilters = (
|
|
|
83
100
|
|
|
84
101
|
return handleSorting(filterCopy)
|
|
85
102
|
})
|
|
103
|
+
return result
|
|
86
104
|
}
|
|
@@ -53,7 +53,31 @@ export const getParentParams = (
|
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Checks if any parent filters are unselected or at their reset state.
|
|
58
|
+
* Returns true if at least one parent is not properly selected.
|
|
59
|
+
*/
|
|
60
|
+
export const hasUnselectedParents = (parentParams, sharedFilters?: SharedFilter[]): boolean => {
|
|
61
|
+
if (!parentParams) return false
|
|
62
|
+
|
|
63
|
+
return parentParams.some(({ key, value }) => {
|
|
64
|
+
// Check if value is empty
|
|
65
|
+
if (value === '') return true
|
|
66
|
+
|
|
67
|
+
// Check if value equals the parent filter's resetLabel
|
|
68
|
+
if (sharedFilters) {
|
|
69
|
+
const parentFilter = sharedFilters.find(f => f.queryParameter === key || f.apiFilter?.valueSelector === key)
|
|
70
|
+
if (parentFilter?.resetLabel && value === parentFilter.resetLabel) {
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Keep old name for backward compatibility
|
|
80
|
+
export const notAllParentsSelected = hasUnselectedParents
|
|
57
81
|
|
|
58
82
|
export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
|
|
59
83
|
const { textSelector, valueSelector, subgroupTextSelector, subgroupValueSelector } = apiFilter
|
|
@@ -86,7 +110,7 @@ export const getToFetch = (
|
|
|
86
110
|
if (apiFilterDropdowns[_key]) return // don't reload cached filter
|
|
87
111
|
const parentParams = getParentParams(filter, sharedFilters)
|
|
88
112
|
|
|
89
|
-
if (notAllParentsSelected(parentParams)) return // don't send request for dependent children filter options
|
|
113
|
+
if (notAllParentsSelected(parentParams, sharedFilters)) return // don't send request for dependent children filter options
|
|
90
114
|
|
|
91
115
|
const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
|
|
92
116
|
toFetch[endpoint] = [_key, index]
|
|
@@ -1,65 +1,67 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
|
-
import { FilterBehavior } from '../helpers/FilterBehavior'
|
|
3
|
-
import {
|
|
4
|
-
getQueryParams,
|
|
5
|
-
removeQueryParam,
|
|
6
|
-
updateQueryParam,
|
|
7
|
-
updateQueryString
|
|
8
|
-
} from '@cdc/core/helpers/queryStringUtils'
|
|
9
|
-
import { SharedFilter } from '../types/SharedFilter'
|
|
10
|
-
import { DashboardFilters } from '../types/DashboardFilters'
|
|
11
|
-
import { FILTER_STYLE } from '../types/FilterStyles'
|
|
12
|
-
|
|
13
|
-
const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
|
|
14
|
-
const parentKey = sharedFilters[parentIndex].key
|
|
15
|
-
const childFilterIndexes = sharedFilters
|
|
16
|
-
.map((filter, index) => (filter.parents?.includes(parentKey) ? index : null))
|
|
17
|
-
.filter(i => i !== null)
|
|
18
|
-
if (childFilterIndexes.length) {
|
|
19
|
-
childFilterIndexes.forEach(filterIndex => {
|
|
20
|
-
const cur = sharedFilters[filterIndex]
|
|
21
|
-
if (cur.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
cur.subGrouping
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
sharedFiltersCopy[filterIndex].active = value
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
queryParams[currentFilter.setByQueryParameter]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { FilterBehavior } from '../helpers/FilterBehavior'
|
|
3
|
+
import {
|
|
4
|
+
getQueryParams,
|
|
5
|
+
removeQueryParam,
|
|
6
|
+
updateQueryParam,
|
|
7
|
+
updateQueryString
|
|
8
|
+
} from '@cdc/core/helpers/queryStringUtils'
|
|
9
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
10
|
+
import { DashboardFilters } from '../types/DashboardFilters'
|
|
11
|
+
import { FILTER_STYLE } from '../types/FilterStyles'
|
|
12
|
+
|
|
13
|
+
const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
|
|
14
|
+
const parentKey = sharedFilters[parentIndex].key
|
|
15
|
+
const childFilterIndexes = sharedFilters
|
|
16
|
+
.map((filter, index) => (filter.parents?.includes(parentKey) ? index : null))
|
|
17
|
+
.filter(i => i !== null)
|
|
18
|
+
if (childFilterIndexes.length) {
|
|
19
|
+
childFilterIndexes.forEach(filterIndex => {
|
|
20
|
+
const cur = sharedFilters[filterIndex]
|
|
21
|
+
if (cur.type === 'urlfilter') {
|
|
22
|
+
if (cur.setByQueryParameter) removeQueryParam(cur.setByQueryParameter)
|
|
23
|
+
cur.active = ''
|
|
24
|
+
if (cur.subGrouping) {
|
|
25
|
+
cur.subGrouping.active = ''
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
return childFilterIndexes
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const changeFilterActive = (
|
|
34
|
+
filterIndex: number,
|
|
35
|
+
value: string | string[],
|
|
36
|
+
sharedFilters: SharedFilter[],
|
|
37
|
+
vizConfig: DashboardFilters
|
|
38
|
+
): [SharedFilter[], number[]] => {
|
|
39
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
40
|
+
const currentFilter = sharedFiltersCopy[filterIndex]
|
|
41
|
+
if (vizConfig.filterBehavior !== FilterBehavior.Apply || vizConfig.autoLoad) {
|
|
42
|
+
if (currentFilter?.filterStyle === FILTER_STYLE.nestedDropdown) {
|
|
43
|
+
sharedFiltersCopy[filterIndex].active = value[0]
|
|
44
|
+
sharedFiltersCopy[filterIndex].subGrouping.active = value[1]
|
|
45
|
+
} else {
|
|
46
|
+
sharedFiltersCopy[filterIndex].active = value
|
|
47
|
+
handleChildren(sharedFiltersCopy, filterIndex)
|
|
48
|
+
const queryParams = getQueryParams()
|
|
49
|
+
if (
|
|
50
|
+
currentFilter.setByQueryParameter &&
|
|
51
|
+
queryParams[currentFilter.setByQueryParameter] !== currentFilter.active
|
|
52
|
+
) {
|
|
53
|
+
queryParams[currentFilter.setByQueryParameter] = currentFilter.active
|
|
54
|
+
updateQueryString(queryParams)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else if (currentFilter.subGrouping) {
|
|
58
|
+
updateQueryParam(currentFilter.setByQueryParameter, value[0])
|
|
59
|
+
updateQueryParam(currentFilter.subGrouping.setByQueryParameter, value[1])
|
|
60
|
+
sharedFiltersCopy[filterIndex].queuedActive = value
|
|
61
|
+
} else {
|
|
62
|
+
const paramVal = Array.isArray(value) ? value.join(',') : value
|
|
63
|
+
if (currentFilter.setByQueryParameter) updateQueryParam(currentFilter.setByQueryParameter, paramVal)
|
|
64
|
+
sharedFiltersCopy[filterIndex].queuedActive = value
|
|
65
|
+
}
|
|
66
|
+
return [sharedFiltersCopy, handleChildren(sharedFiltersCopy, filterIndex)]
|
|
67
|
+
}
|