@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.
Files changed (86) hide show
  1. package/Dynamic_Data.md +66 -0
  2. package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
  3. package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
  4. package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
  5. package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
  6. package/dist/cdcdashboard.js +84214 -79641
  7. package/examples/api-dashboard-data.json +272 -0
  8. package/examples/api-dashboard-years.json +11 -0
  9. package/examples/api-geographies-data.json +11 -0
  10. package/examples/api-test/categories.json +18 -0
  11. package/examples/api-test/chart-data.json +602 -0
  12. package/examples/api-test/topics.json +47 -0
  13. package/examples/api-test/years.json +22 -0
  14. package/examples/markup-axis-label.json +4167 -0
  15. package/examples/private/big-dashboard.json +39095 -39077
  16. package/examples/private/cat-y.json +1235 -0
  17. package/examples/private/chronic-dash.json +1584 -0
  18. package/examples/private/clade-2.json +430 -0
  19. package/examples/private/diabetes.json +546 -196
  20. package/examples/private/map-issue.json +2260 -0
  21. package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
  22. package/examples/private/mpinc-state-reports.json +2260 -0
  23. package/examples/private/mpox.json +38128 -0
  24. package/examples/private/nwss/rsv.json +1240 -0
  25. package/examples/private/reset.json +32920 -0
  26. package/examples/private/simple-dash.json +490 -0
  27. package/examples/private/test-dash.json +0 -0
  28. package/examples/private/test123.json +491 -0
  29. package/examples/test-api-filter-reset.json +132 -0
  30. package/examples/test-dashboard-simple.json +503 -0
  31. package/index.html +25 -26
  32. package/package.json +11 -11
  33. package/src/CdcDashboardComponent.tsx +35 -10
  34. package/src/DashboardContext.tsx +3 -1
  35. package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
  36. package/src/_stories/Dashboard.stories.tsx +402 -1
  37. package/src/_stories/_mock/custom-order-new-values.json +116 -0
  38. package/src/_stories/_mock/filter-cascade.json +3350 -0
  39. package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
  40. package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
  41. package/src/_stories/_mock/parent-child-filters.json +233 -0
  42. package/src/components/DashboardFilters/DashboardFilters.tsx +54 -31
  43. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +118 -50
  44. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +96 -108
  45. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +196 -59
  46. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +129 -29
  47. package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +62 -3
  48. package/src/components/DataDesignerModal.tsx +18 -6
  49. package/src/components/Header/Header.tsx +53 -21
  50. package/src/components/Toggle/Toggle.tsx +48 -48
  51. package/src/components/VisualizationRow.tsx +73 -6
  52. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
  53. package/src/components/Widget/Widget.tsx +1 -1
  54. package/src/data/initial-state.js +1 -0
  55. package/src/helpers/addValuesToDashboardFilters.ts +24 -6
  56. package/src/helpers/apiFilterHelpers.ts +26 -2
  57. package/src/helpers/changeFilterActive.ts +67 -65
  58. package/src/helpers/filterData.ts +52 -7
  59. package/src/helpers/filterResetHelpers.ts +102 -0
  60. package/src/helpers/formatConfigBeforeSave.ts +6 -5
  61. package/src/helpers/getUpdateConfig.ts +91 -91
  62. package/src/helpers/getVizConfig.ts +2 -2
  63. package/src/helpers/loadAPIFilters.ts +109 -99
  64. package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
  65. package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
  66. package/src/helpers/updateChildFilters.ts +50 -27
  67. package/src/index.tsx +1 -0
  68. package/src/scss/editor-panel.scss +3 -431
  69. package/src/scss/main.scss +142 -25
  70. package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
  71. package/src/test/CdcDashboard.test.jsx +9 -4
  72. package/src/types/Dashboard.ts +1 -0
  73. package/src/types/DashboardFilters.ts +9 -8
  74. package/src/types/FilterStyles.ts +8 -7
  75. package/src/types/SharedFilter.ts +13 -0
  76. package/LICENSE +0 -201
  77. package/examples/private/DEV-11072.json +0 -7591
  78. package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +0 -14041
  79. package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +0 -14041
  80. package/examples/private/burden_toolkit_mortality_qaly_data.csv +0 -18721
  81. package/examples/private/burden_toolkit_mortality_yll_data.csv +0 -18721
  82. package/examples/private/pedro.json +0 -1
  83. package/src/helpers/getAutoLoadVisualization.ts +0 -11
  84. package/src/scss/mixins.scss +0 -47
  85. package/src/scss/variables.scss +0 -5
  86. /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:&nbsp;
137
- <select className='dataset-selector' value={configureData.dataKey || ''} onChange={changeDataset}>
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 => <option key={datasetKey}>{datasetKey}</option>)}
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
- <InputSelect
190
- options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})}
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 { useEffect, useContext } from 'react'
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
- newConfig.visualizations[visualizationKey].editing = false
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
- useEffect(() => {
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
- // Pass up to Editor if needed
68
- if (setParentConfig) {
69
- setParentConfig(parsedData)
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
- <input
100
- type='text'
101
- placeholder='Enter Dashboard Name Here'
102
- defaultValue={config.dashboard?.title}
103
- onChange={e => changeConfigValue('dashboard', 'title', e.target.value)}
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 ${type}`}
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 className={`row${row.equalHeight ? ' equal-height' : ''}${row.toggle ? ' toggle' : ''}`} key={`row__${index}`}>
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) return <div key={`row__${index}__col__${colIndex}`} className={`col col-${col.width}`}></div>
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
- sharedFilterIndexes.length
227
- const hasMarginBottom = !isLastRow && !hiddenDashboardFilters
293
+ sharedFilterIndexes.length
228
294
 
229
- const vizWrapperClass = `col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${hideVisualization ? ' hide-parent-visualization' : hasMarginBottom ? ' mb-4' : ''
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,6 +1,7 @@
1
1
  export default {
2
2
  dashboard: {
3
3
  theme: 'theme-blue',
4
+ titleStyle: 'small',
4
5
  sharedFilters: []
5
6
  },
6
7
  rows: [[{ width: 12 }, {}, {}]],
@@ -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
- return filters?.map((filter, index) => {
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
- const filterValues = generateValuesForFilter(getSelector(filter), data)
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
- const hasResetLabel = filters.find(filter => filter.resetLabel)
54
- const defaultValue = hasResetLabel ? hasResetLabel.resetLabel : filterCopy.active || filterCopy.values[0]
55
- filterCopy.active = filterCopy.defaultValue || defaultValue
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
- export const notAllParentsSelected = parentParams => parentParams?.some(({ value }) => value === '')
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.setByQueryParameter) removeQueryParam(cur.setByQueryParameter)
22
- cur.active = ''
23
- if (cur.subGrouping) {
24
- cur.subGrouping.active = ''
25
- }
26
- })
27
- }
28
- return childFilterIndexes
29
- }
30
-
31
- export const changeFilterActive = (
32
- filterIndex: number,
33
- value: string | string[],
34
- sharedFilters: SharedFilter[],
35
- vizConfig: DashboardFilters
36
- ): [SharedFilter[], number[]] => {
37
- const sharedFiltersCopy = _.cloneDeep(sharedFilters)
38
- const currentFilter = sharedFiltersCopy[filterIndex]
39
- if (vizConfig.filterBehavior !== FilterBehavior.Apply || vizConfig.autoLoad) {
40
- if (currentFilter?.filterStyle === FILTER_STYLE.nestedDropdown) {
41
- sharedFiltersCopy[filterIndex].active = value[0]
42
- sharedFiltersCopy[filterIndex].subGrouping.active = value[1]
43
- } else {
44
- sharedFiltersCopy[filterIndex].active = value
45
- handleChildren(sharedFiltersCopy, filterIndex)
46
- const queryParams = getQueryParams()
47
- if (
48
- currentFilter.setByQueryParameter &&
49
- queryParams[currentFilter.setByQueryParameter] !== currentFilter.active
50
- ) {
51
- queryParams[currentFilter.setByQueryParameter] = currentFilter.active
52
- updateQueryString(queryParams)
53
- }
54
- }
55
- } else if (currentFilter.subGrouping) {
56
- updateQueryParam(currentFilter.setByQueryParameter, value[0])
57
- updateQueryParam(currentFilter.subGrouping.setByQueryParameter, value[1])
58
- sharedFiltersCopy[filterIndex].queuedActive = value
59
- } else {
60
- const paramVal = Array.isArray(value) ? value.join(',') : value
61
- if (currentFilter.setByQueryParameter) updateQueryParam(currentFilter.setByQueryParameter, paramVal)
62
- sharedFiltersCopy[filterIndex].queuedActive = value
63
- }
64
- return [sharedFiltersCopy, handleChildren(sharedFiltersCopy, filterIndex)]
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
+ }