@cdc/dashboard 4.25.11 → 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 (51) hide show
  1. package/Dynamic_Data.md +66 -0
  2. package/dist/cdcdashboard.js +78783 -76370
  3. package/examples/api-dashboard-data.json +272 -0
  4. package/examples/api-dashboard-years.json +11 -0
  5. package/examples/api-geographies-data.json +11 -0
  6. package/examples/private/cat-y.json +1235 -0
  7. package/examples/private/chronic-dash.json +1584 -0
  8. package/examples/private/map-issue.json +2260 -0
  9. package/examples/private/mpinc-state-reports.json +2260 -0
  10. package/examples/private/nwss/rsv.json +1240 -0
  11. package/examples/private/simple-dash.json +490 -0
  12. package/examples/private/test-dash.json +0 -0
  13. package/examples/private/test123.json +491 -0
  14. package/examples/test-dashboard-simple.json +503 -0
  15. package/index.html +24 -25
  16. package/package.json +12 -11
  17. package/src/CdcDashboardComponent.tsx +18 -2
  18. package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
  19. package/src/_stories/Dashboard.stories.tsx +385 -1
  20. package/src/_stories/_mock/filter-cascade.json +3350 -0
  21. package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
  22. package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
  23. package/src/_stories/_mock/parent-child-filters.json +233 -0
  24. package/src/components/DashboardFilters/DashboardFilters.tsx +20 -11
  25. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +89 -38
  26. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +51 -29
  27. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +146 -9
  28. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +11 -7
  29. package/src/components/DataDesignerModal.tsx +6 -1
  30. package/src/components/Header/Header.tsx +51 -20
  31. package/src/components/VisualizationRow.tsx +71 -5
  32. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
  33. package/src/components/Widget/Widget.tsx +1 -1
  34. package/src/data/initial-state.js +1 -0
  35. package/src/helpers/addValuesToDashboardFilters.ts +15 -22
  36. package/src/helpers/changeFilterActive.ts +67 -65
  37. package/src/helpers/formatConfigBeforeSave.ts +6 -5
  38. package/src/helpers/getUpdateConfig.ts +91 -91
  39. package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
  40. package/src/helpers/updateChildFilters.ts +50 -27
  41. package/src/scss/main.scss +141 -1
  42. package/src/test/CdcDashboard.test.jsx +9 -4
  43. package/src/types/Dashboard.ts +1 -0
  44. package/src/types/FilterStyles.ts +8 -7
  45. package/src/types/SharedFilter.ts +13 -0
  46. package/LICENSE +0 -201
  47. package/examples/private/DEV-10538.json +0 -407
  48. package/examples/private/DEV-11072.json +0 -7591
  49. package/examples/private/DEV-11405.json +0 -39112
  50. package/examples/private/delete.json +0 -32919
  51. package/examples/private/pedro.json +0 -1
@@ -21,6 +21,7 @@ import { addValuesToDashboardFilters } from '../../../helpers/addValuesToDashboa
21
21
  import { FILTER_STYLE } from '../../../types/FilterStyles'
22
22
  import { handleSorting } from '@cdc/core/components/Filters'
23
23
  import { removeDashboardFilter } from '../../../helpers/removeDashboardFilter'
24
+ import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd'
24
25
 
25
26
  type DashboardFitlersEditorProps = {
26
27
  vizConfig: DashboardFilters
@@ -71,6 +72,7 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
71
72
  newSharedFilters[index][prop] = value
72
73
  if (prop === 'columnName') {
73
74
  if (newSharedFilters[index].subGrouping) delete newSharedFilters[index].subGrouping
75
+ newSharedFilters[index].defaultValue = ''
74
76
  // changing a data column and want to load the data into the preview options
75
77
  const sharedFiltersWithValues = addValuesToDashboardFilters(newSharedFilters, data)
76
78
  dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFiltersWithValues })
@@ -122,6 +124,29 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
122
124
  dispatch({ type: 'SET_CONFIG', payload: { dashboard, visualizations: newVisualizations } })
123
125
  }
124
126
 
127
+ const handleFilterReorder = (result: DropResult) => {
128
+ const { source, destination } = result
129
+ if (!destination || source.index === destination.index) return
130
+
131
+ const newIndexes = [...vizConfig.sharedFilterIndexes]
132
+ const [movedIndex] = newIndexes.splice(source.index, 1)
133
+ newIndexes.splice(destination.index, 0, movedIndex)
134
+
135
+ updateConfig({
136
+ ...vizConfig,
137
+ sharedFilterIndexes: newIndexes
138
+ })
139
+ }
140
+
141
+ const getItemStyle = (isDragging, draggableStyle) => ({
142
+ ...draggableStyle,
143
+ ...(isDragging && sortableItemStyles)
144
+ })
145
+
146
+ const sortableItemStyles = {
147
+ background: 'rgba(0, 0, 0, 0.1)'
148
+ }
149
+
125
150
  const addNewFilter = () => {
126
151
  const _sharedFilters = _.cloneDeep(sharedFilters) || []
127
152
  const columnName = 'New Dashboard Filter ' + (_sharedFilters.length + 1)
@@ -219,44 +244,70 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
219
244
  <AccordionItemButton>Filters</AccordionItemButton>
220
245
  </AccordionItemHeading>
221
246
  <AccordionItemPanel>
222
- {vizConfig.sharedFilterIndexes.map(index => {
223
- const filter = sharedFilters[index]
224
- return (
225
- <FieldSetWrapper
226
- key={filter.key + index}
227
- fieldName={filter.key}
228
- fieldKey={index}
229
- fieldType='Dashboard Filter'
230
- controls={openControls}
231
- deleteField={() => {
232
- overlay?.actions.openOverlay(
233
- <DeleteFilterModal
234
- removeFilterCompletely={removeFilter}
235
- removeFilterFromViz={index => {
236
- updateConfig({
237
- ...vizConfig,
238
- sharedFilterIndexes: vizConfig.sharedFilterIndexes.filter(i => i !== index)
239
- })
240
- }}
241
- filterIndex={index}
242
- />
243
- )
244
- }}
245
- >
246
- <FilterEditor
247
- filter={filter}
248
- filterIndex={index}
249
- updateFilterProp={(name, value) => {
250
- updateFilterProp(name, index, value)
251
- }}
252
- toggleNestedQueryParameters={checked => {
253
- toggleNestedQueryParameters(index, checked)
254
- }}
255
- config={config}
256
- />
257
- </FieldSetWrapper>
258
- )
259
- })}
247
+ <DragDropContext onDragEnd={handleFilterReorder}>
248
+ <Droppable droppableId='dashboard_filters_list'>
249
+ {provided => (
250
+ <ul {...provided.droppableProps} ref={provided.innerRef} className='draggable-field-list'>
251
+ {vizConfig.sharedFilterIndexes.map((index, filterIndex) => {
252
+ const filter = sharedFilters[index]
253
+ return (
254
+ <Draggable
255
+ key={filter.key + index}
256
+ draggableId={`filter-${filter.key}-${index}`}
257
+ index={filterIndex}
258
+ >
259
+ {(provided, snapshot) => (
260
+ <div
261
+ ref={provided.innerRef}
262
+ {...provided.draggableProps}
263
+ {...provided.dragHandleProps}
264
+ style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
265
+ className={snapshot.isDragging ? 'currently-dragging' : ''}
266
+ >
267
+ <FieldSetWrapper
268
+ key={filter.key + index}
269
+ fieldName={filter.key}
270
+ fieldKey={index}
271
+ fieldType='Dashboard Filter'
272
+ controls={openControls}
273
+ draggable={true}
274
+ deleteField={() => {
275
+ overlay?.actions.openOverlay(
276
+ <DeleteFilterModal
277
+ removeFilterCompletely={removeFilter}
278
+ removeFilterFromViz={index => {
279
+ updateConfig({
280
+ ...vizConfig,
281
+ sharedFilterIndexes: vizConfig.sharedFilterIndexes.filter(i => i !== index)
282
+ })
283
+ }}
284
+ filterIndex={index}
285
+ />
286
+ )
287
+ }}
288
+ >
289
+ <FilterEditor
290
+ filter={filter}
291
+ filterIndex={index}
292
+ updateFilterProp={(name, value) => {
293
+ updateFilterProp(name, index, value)
294
+ }}
295
+ toggleNestedQueryParameters={checked => {
296
+ toggleNestedQueryParameters(index, checked)
297
+ }}
298
+ config={config}
299
+ />
300
+ </FieldSetWrapper>
301
+ </div>
302
+ )}
303
+ </Draggable>
304
+ )
305
+ })}
306
+ {provided.placeholder}
307
+ </ul>
308
+ )}
309
+ </Droppable>
310
+ </DragDropContext>
260
311
  <button onClick={addNewFilter} className='btn btn-primary full-width'>
261
312
  Add Filter
262
313
  </button>
@@ -43,7 +43,11 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
43
43
  const filterStyles = Object.values(FILTER_STYLE)
44
44
 
45
45
  const parentFilters: string[] = (config.dashboard.sharedFilters || [])
46
- .filter(({ key, type }) => key !== filter.key && type !== 'datafilter')
46
+ .filter(({ key }) => key !== filter.key)
47
+ .map(({ key }) => key)
48
+
49
+ const dataFilterParents: string[] = (config.dashboard.sharedFilters || [])
50
+ .filter(({ key }) => key !== filter.key)
47
51
  .map(({ key }) => key)
48
52
 
49
53
  const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
@@ -246,21 +250,6 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
246
250
  <>
247
251
  {!hasDashboardApplyBehavior(config.visualizations) && (
248
252
  <>
249
- <Select
250
- label='URL to Filter'
251
- value={filter.datasetKey || ''}
252
- options={[
253
- { value: '', label: '- Select Option -' },
254
- ...Object.keys(config.datasets)
255
- .filter(datasetKey => config.datasets[datasetKey].dataUrl)
256
- .map(datasetKey => ({
257
- value: datasetKey,
258
- label: config.datasets[datasetKey].dataUrl
259
- }))
260
- ]}
261
- onChange={e => updateFilterProp('datasetKey', e.target.value)}
262
- />
263
-
264
253
  <Select
265
254
  label='Filter By'
266
255
  value={filter.filterBy || ''}
@@ -271,6 +260,40 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
271
260
  ]}
272
261
  onChange={e => updateFilterProp('filterBy', e.target.value)}
273
262
  />
263
+
264
+ {filter.filterBy === 'File Name' && (
265
+ <Select
266
+ label='URL to Filter'
267
+ value={filter.datasetKey || ''}
268
+ options={[
269
+ { value: '', label: '- Select Option -' },
270
+ ...Object.keys(config.datasets)
271
+ .filter(datasetKey => config.datasets[datasetKey].dataUrl)
272
+ .map(datasetKey => ({
273
+ value: datasetKey,
274
+ label: config.datasets[datasetKey].dataUrl
275
+ }))
276
+ ]}
277
+ onChange={e => updateFilterProp('datasetKey', e.target.value)}
278
+ tooltip={
279
+ <Tooltip style={{ textTransform: 'none' }}>
280
+ <Tooltip.Target>
281
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
282
+ </Tooltip.Target>
283
+ <Tooltip.Content>
284
+ <p>Select which dataset URL's filename should be modified by this filter.</p>
285
+ </Tooltip.Content>
286
+ </Tooltip>
287
+ }
288
+ />
289
+ )}
290
+
291
+ {filter.filterBy === 'Query String' && filter.usedBy && filter.usedBy.length > 0 && (
292
+ <div className='bg-info-subtle p-2 my-2' style={{ fontSize: '0.9em' }}>
293
+ <Icon display='info' style={{ marginRight: '0.5rem' }} />
294
+ Will apply to datasets used by selected widgets
295
+ </div>
296
+ )}
274
297
  {filter.filterBy === 'File Name' && (
275
298
  <>
276
299
  <TextField
@@ -617,19 +640,18 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
617
640
  updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)}
618
641
  />
619
642
 
620
- <Select
621
- label='Parent Filter'
622
- value={filter.parents || ''}
623
- options={[
624
- { value: '', label: 'Select a filter' },
625
- ...(config.dashboard.sharedFilters || [])
626
- .filter(sharedFilter => sharedFilter.key !== filter.key)
627
- .map(sharedFilter => ({ value: sharedFilter.key, label: sharedFilter.key }))
628
- ]}
629
- onChange={e => {
630
- updateFilterProp('parents', e.target.value)
631
- }}
632
- />
643
+ <label>
644
+ <span className='edit-label column-heading mt-1'>Parent Filter(s): </span>
645
+ <MultiSelect
646
+ label='Parent Filter(s): '
647
+ options={dataFilterParents.map(key => ({ value: key, label: key }))}
648
+ fieldName='parents'
649
+ selected={filter.parents}
650
+ updateField={(_section, _subsection, _fieldname, newItems) => {
651
+ updateFilterProp('parents', newItems)
652
+ }}
653
+ />
654
+ </label>
633
655
 
634
656
  {!isNestedDropdown && (
635
657
  <TextField
@@ -1,8 +1,11 @@
1
1
  import { DashboardConfig } from '../../../../types/DashboardConfig'
2
2
  import { SharedFilter } from '../../../../types/SharedFilter'
3
3
  import _ from 'lodash'
4
- import { SubGrouping } from '@cdc/core/types/VizFilter'
4
+ import { SubGrouping, OrderBy } from '@cdc/core/types/VizFilter'
5
5
  import { TextField, Select } from '@cdc/core/components/EditorPanel/Inputs'
6
+ import { handleSorting } from '@cdc/core/components/Filters/helpers/handleSorting'
7
+ import { filterOrderOptions } from '@cdc/core/helpers/filterOrderOptions'
8
+ import FilterOrder from '@cdc/core/components/EditorPanel/VizFilterEditor/components/FilterOrder'
6
9
 
7
10
  type NestedDropDownEditorDashboardProps = {
8
11
  config: DashboardConfig
@@ -50,12 +53,10 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
50
53
  const handleFitlerGroupColumnNameChange = (value: string) => {
51
54
  if (!value) {
52
55
  updateFilterProp('columnName', '')
53
- updateFilterProp('defaultValue', '')
54
56
  return
55
57
  }
56
58
  const [newColumnName, selectedOptionDatasetName] = value.split('|')
57
59
  updateFilterProp('columnName', newColumnName)
58
- updateFilterProp('defaultValue', '') // Reset default value when column changes
59
60
  populateSubGroupingOptions(selectedOptionDatasetName, newColumnName)
60
61
  }
61
62
 
@@ -66,18 +67,23 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
66
67
  }
67
68
  const [newColumnName, selectedOptionDatasetName] = value.split('|')
68
69
 
70
+ const order = subGrouping?.order || 'asc'
71
+
69
72
  const valuesLookup = filter.values.reduce((acc, groupName) => {
70
- const values: string[] = _.uniq(
73
+ const rawValues: string[] = _.uniq(
71
74
  config.datasets[selectedOptionDatasetName].data
72
75
  .map(d => {
73
76
  return d[filter.columnName] === groupName ? d[newColumnName] : ''
74
77
  })
75
78
  .filter(value => value !== '')
76
- ).sort()
79
+ )
80
+
81
+ // Sort values according to the order setting
82
+ const { values: sortedValues } = handleSorting({ values: rawValues, order })
77
83
 
78
84
  acc[groupName] = {
79
- values,
80
- orderedValues: values
85
+ values: sortedValues,
86
+ orderedValues: sortedValues
81
87
  }
82
88
  return acc
83
89
  }, {})
@@ -86,12 +92,94 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
86
92
  ...subGrouping,
87
93
  columnName: newColumnName,
88
94
  valuesLookup,
95
+ order,
89
96
  defaultValue: '' // Reset default value when column changes
90
97
  }
91
98
 
92
99
  updateFilterProp('subGrouping', newSubGrouping)
93
100
  }
94
101
 
102
+ // Handle group order change (asc/desc/cust)
103
+ const handleGroupingOrderBy = (order: OrderBy) => {
104
+ const groupSortObject = {
105
+ values: _.cloneDeep(filter.values),
106
+ order
107
+ }
108
+ const { values: newOrderedValues } = handleSorting(groupSortObject)
109
+
110
+ const updates: Partial<SharedFilter> = {
111
+ values: newOrderedValues,
112
+ order
113
+ }
114
+
115
+ if (order === 'cust') {
116
+ updates.orderedValues = newOrderedValues
117
+ } else {
118
+ updates.orderedValues = undefined
119
+ }
120
+
121
+ // Update filter with new order and values
122
+ updateFilterProp('order', order)
123
+ }
124
+
125
+ // Handle drag-drop reorder for group values
126
+ const handleGroupingCustomOrder = (sourceIndex: number, destinationIndex: number) => {
127
+ if (sourceIndex === undefined || destinationIndex === undefined || sourceIndex === destinationIndex) return
128
+
129
+ const orderedValues = _.cloneDeep(filter.orderedValues || filter.values)
130
+ const [movedItem] = orderedValues.splice(sourceIndex, 1)
131
+ orderedValues.splice(destinationIndex, 0, movedItem)
132
+
133
+ // Update both values and orderedValues, and ensure order is 'cust'
134
+ updateFilterProp('orderedValues', orderedValues)
135
+ if (filter.order !== 'cust') {
136
+ updateFilterProp('order', 'cust')
137
+ }
138
+ }
139
+
140
+ // Handle subgroup order change (asc/desc/cust)
141
+ const handleSubGroupingOrderBy = (order: OrderBy) => {
142
+ const newValuesLookup = Object.keys(subGrouping.valuesLookup).reduce((acc, groupName) => {
143
+ const subGroup = subGrouping.valuesLookup[groupName]
144
+ const { values: sortedValues } = handleSorting({ values: _.cloneDeep(subGroup.values), order })
145
+
146
+ acc[groupName] = {
147
+ values: sortedValues,
148
+ orderedValues: order === 'cust' ? sortedValues : undefined
149
+ }
150
+ return acc
151
+ }, {})
152
+
153
+ const newSubGrouping: SubGrouping = {
154
+ ...subGrouping,
155
+ order,
156
+ valuesLookup: newValuesLookup
157
+ }
158
+
159
+ updateFilterProp('subGrouping', newSubGrouping)
160
+ }
161
+
162
+ // Handle drag-drop reorder for subgroup values within a specific group
163
+ const handleSubGroupingCustomOrder = (
164
+ sourceIndex: number,
165
+ destinationIndex: number,
166
+ currentOrderedValues: string[],
167
+ groupName: string
168
+ ) => {
169
+ if (sourceIndex === undefined || destinationIndex === undefined || sourceIndex === destinationIndex) return
170
+
171
+ const updatedGroupOrderedValues = _.cloneDeep(currentOrderedValues)
172
+ const [movedItem] = updatedGroupOrderedValues.splice(sourceIndex, 1)
173
+ updatedGroupOrderedValues.splice(destinationIndex, 0, movedItem)
174
+
175
+ const newSubGrouping = _.cloneDeep(subGrouping)
176
+ newSubGrouping.valuesLookup[groupName].values = updatedGroupOrderedValues
177
+ newSubGrouping.valuesLookup[groupName].orderedValues = updatedGroupOrderedValues
178
+ newSubGrouping.order = 'cust'
179
+
180
+ updateFilterProp('subGrouping', newSubGrouping)
181
+ }
182
+
95
183
  return (
96
184
  <div className='nesteddropdown-editor'>
97
185
  {!isDashboard && (
@@ -144,7 +232,7 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
144
232
  {filter.columnName && filter.values && filter.values.length > 0 && (
145
233
  <Select
146
234
  value={filter.defaultValue}
147
- options={filter.values}
235
+ options={filter.orderedValues || filter.values}
148
236
  updateField={(_section, _subSection, _key, value) => updateFilterProp('defaultValue', value)}
149
237
  label={'Group Default Value'}
150
238
  initial={'Select'}
@@ -157,7 +245,8 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
157
245
  value={subGrouping.defaultValue}
158
246
  options={(() => {
159
247
  const groupKey = filter.defaultValue || (Array.isArray(filter.active) ? filter.active[0] : filter.active)
160
- return subGrouping.valuesLookup[groupKey as string]?.values || []
248
+ const lookup = subGrouping.valuesLookup[groupKey as string]
249
+ return lookup?.orderedValues || lookup?.values || []
161
250
  })()}
162
251
  updateField={(_section, _subSection, _key, value) => {
163
252
  const newSubGrouping = { ...subGrouping, defaultValue: value }
@@ -167,6 +256,54 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
167
256
  initial={'Select'}
168
257
  />
169
258
  )}
259
+
260
+ {/* Group Order */}
261
+ {filter.columnName && filter.values && filter.values.length > 0 && (
262
+ <div className='mt-2'>
263
+ <Select
264
+ label='Group Order'
265
+ value={filter.order || 'asc'}
266
+ options={filterOrderOptions}
267
+ onChange={e => handleGroupingOrderBy(e.target.value as OrderBy)}
268
+ />
269
+ {filter.order === 'cust' && (
270
+ <FilterOrder
271
+ orderedValues={filter.orderedValues || filter.values}
272
+ handleFilterOrder={handleGroupingCustomOrder}
273
+ />
274
+ )}
275
+ </div>
276
+ )}
277
+
278
+ {/* SubGrouping Order */}
279
+ {subGrouping?.columnName && subGrouping.valuesLookup && Object.keys(subGrouping.valuesLookup).length > 0 && (
280
+ <div className='mt-2'>
281
+ <Select
282
+ label='SubGrouping Order'
283
+ value={subGrouping.order || 'asc'}
284
+ options={filterOrderOptions}
285
+ onChange={e => handleSubGroupingOrderBy(e.target.value as OrderBy)}
286
+ />
287
+ {subGrouping.order === 'cust' &&
288
+ (filter.orderedValues || filter.values)?.map((groupName, i) => {
289
+ const lookup = subGrouping.valuesLookup[groupName]
290
+ if (!lookup) return null
291
+ const orderedSubGroupValues = lookup.orderedValues || lookup.values
292
+ return (
293
+ <div key={`group-subgroup-values-${groupName}-${i}`}>
294
+ <span className='font-weight-bold fw-bold'>{groupName}</span>
295
+ <FilterOrder
296
+ key={`subgroup-values-${groupName}-${i}`}
297
+ orderedValues={orderedSubGroupValues}
298
+ handleFilterOrder={(sourceIndex, destinationIndex) => {
299
+ handleSubGroupingCustomOrder(sourceIndex, destinationIndex, orderedSubGroupValues, groupName)
300
+ }}
301
+ />
302
+ </div>
303
+ )
304
+ })}
305
+ </div>
306
+ )}
170
307
  </div>
171
308
  )
172
309
  }
@@ -177,15 +177,18 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
177
177
  // Reset filtersApplied state to false when clearing filters
178
178
  dispatch({ type: 'SET_FILTERS_APPLIED', payload: false })
179
179
 
180
+ // Update child filter values before filtering data
181
+ const updatedFilters = updateChildFilters(dashboardConfig.sharedFilters, state.data)
182
+
180
183
  // Update filtered data immediately after resetting filters
181
- // Use the updated dashboardConfig filters instead of state
184
+ // Use the updated filters instead of state
182
185
  const clonedState = {
183
186
  ...state,
184
187
  config: {
185
188
  ...state.config,
186
189
  dashboard: {
187
190
  ...state.config.dashboard,
188
- sharedFilters: dashboardConfig.sharedFilters
191
+ sharedFilters: updatedFilters
189
192
  }
190
193
  }
191
194
  }
@@ -262,8 +265,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
262
265
  loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale)
263
266
  }
264
267
  } else {
268
+ const updatedFilters = updateChildFilters(newSharedFilters, state.data)
265
269
  if (newSharedFilters[index].type === 'urlfilter' && newSharedFilters[index].apiFilter) {
266
- reloadURLData(newSharedFilters)
270
+ reloadURLData(updatedFilters)
267
271
  } else {
268
272
  const clonedState = {
269
273
  ...state,
@@ -271,13 +275,13 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
271
275
  ...state.config,
272
276
  dashboard: {
273
277
  ...state.config.dashboard,
274
- sharedFilters: newSharedFilters
278
+ sharedFilters: updatedFilters
275
279
  }
276
280
  }
277
281
  }
278
282
  const newFilteredData = getFilteredData(clonedState)
279
283
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
280
- dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
284
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: updatedFilters })
281
285
  }
282
286
  }
283
287
  }
@@ -293,9 +297,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
293
297
  // if all of the filters are hidden filters don't display the VisualizationWrapper
294
298
  const filters = visualizationConfig?.sharedFilterIndexes
295
299
  ?.map(Number)
296
- .map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
300
+ ?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
297
301
 
298
- const displayNone = filters.length ? filters.every(filter => filter.showDropdown === false) : false
302
+ const displayNone = filters?.length ? filters.every(filter => filter.showDropdown === false) : false
299
303
  if (displayNone && !isEditor) return <></>
300
304
  return (
301
305
  <Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
@@ -194,7 +194,12 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
194
194
  ) : (
195
195
  <>
196
196
  <Select
197
- options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})}
197
+ options={Object.keys(
198
+ config.rows[rowIndex]?.data?.[0] ||
199
+ configureData.data?.[0] ||
200
+ config.datasets[configureData.dataKey]?.data?.[0] ||
201
+ {}
202
+ )}
198
203
  value={config.rows[rowIndex].multiVizColumn}
199
204
  label='Multi-Visualization Column'
200
205
  initial='--Select--'
@@ -1,4 +1,4 @@
1
- import { useContext, useRef } 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
@@ -59,19 +73,21 @@ const Header = (props: HeaderProps) => {
59
73
  const configStringRef = useRef<string>()
60
74
 
61
75
  // Only update parent when config content actually changes (not just reference)
62
- const configString = JSON.stringify(convertStateToConfig())
63
- if (configStringRef.current !== configString) {
64
- configStringRef.current = configString
65
-
66
- // Emit the data in a regular JS event so it can be consumed by anything.
67
- const event = new CustomEvent('updateVizConfig', { detail: configString })
68
- window.dispatchEvent(event)
69
-
70
- // Pass up to Editor if needed
71
- if (setParentConfig) {
72
- setParentConfig(JSON.parse(configString))
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
+ }
73
89
  }
74
- }
90
+ }, [config, setParentConfig])
75
91
 
76
92
  const handleCheck = e => {
77
93
  const { checked } = e.currentTarget
@@ -97,12 +113,27 @@ const Header = (props: HeaderProps) => {
97
113
  multidashboard
98
114
  </span>
99
115
  <br />
100
- <input
101
- type='text'
102
- placeholder='Enter Dashboard Name Here'
103
- defaultValue={config.dashboard?.title}
104
- onChange={e => changeConfigValue('dashboard', 'title', e.target.value)}
105
- />
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>
106
137
  </div>
107
138
  )}
108
139
  {!subEditor && (