@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
@@ -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
@@ -47,30 +50,40 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
47
50
  })
48
51
  }
49
52
 
50
- const handleFitlerGroupColumnNameChange = selectedOption => {
51
- const selectedOptionDatasetName = selectedOption.selectedOptions[0].dataset.set
52
- const newColumnName = selectedOption.value
53
+ const handleFitlerGroupColumnNameChange = (value: string) => {
54
+ if (!value) {
55
+ updateFilterProp('columnName', '')
56
+ return
57
+ }
58
+ const [newColumnName, selectedOptionDatasetName] = value.split('|')
53
59
  updateFilterProp('columnName', newColumnName)
54
- updateFilterProp('defaultValue', '') // Reset default value when column changes
55
60
  populateSubGroupingOptions(selectedOptionDatasetName, newColumnName)
56
61
  }
57
62
 
58
- const handleSubGroupColumnNameChange = selectedOption => {
59
- const selectedOptionDatasetName = selectedOption.selectedOptions[0].dataset.set
60
- const newColumnName = selectedOption.value
63
+ const handleSubGroupColumnNameChange = (value: string) => {
64
+ if (!value) {
65
+ updateFilterProp('subGrouping', { ...subGrouping, columnName: '', valuesLookup: {}, defaultValue: '' })
66
+ return
67
+ }
68
+ const [newColumnName, selectedOptionDatasetName] = value.split('|')
69
+
70
+ const order = subGrouping?.order || 'asc'
61
71
 
62
72
  const valuesLookup = filter.values.reduce((acc, groupName) => {
63
- const values: string[] = _.uniq(
73
+ const rawValues: string[] = _.uniq(
64
74
  config.datasets[selectedOptionDatasetName].data
65
75
  .map(d => {
66
76
  return d[filter.columnName] === groupName ? d[newColumnName] : ''
67
77
  })
68
78
  .filter(value => value !== '')
69
- ).sort()
79
+ )
80
+
81
+ // Sort values according to the order setting
82
+ const { values: sortedValues } = handleSorting({ values: rawValues, order })
70
83
 
71
84
  acc[groupName] = {
72
- values,
73
- orderedValues: values
85
+ values: sortedValues,
86
+ orderedValues: sortedValues
74
87
  }
75
88
  return acc
76
89
  }, {})
@@ -79,12 +92,94 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
79
92
  ...subGrouping,
80
93
  columnName: newColumnName,
81
94
  valuesLookup,
95
+ order,
82
96
  defaultValue: '' // Reset default value when column changes
83
97
  }
84
98
 
85
99
  updateFilterProp('subGrouping', newSubGrouping)
86
100
  }
87
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
+
88
183
  return (
89
184
  <div className='nesteddropdown-editor'>
90
185
  {!isDashboard && (
@@ -94,57 +189,50 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
94
189
  updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)}
95
190
  />
96
191
  )}
97
- <label>
98
- <div className='edit-label column-heading mt-2'>
99
- Filter Grouping
100
- <span></span>
101
- </div>
102
- <select value={filter.columnName} onChange={e => handleFitlerGroupColumnNameChange(e.target)}>
103
- <option value=''>- Select Option -</option>
104
- {columnNameOptionsInDataset?.map(option => (
105
- <option
106
- value={option.columnName}
107
- data-set={option.datasetKey}
108
- key={`filter_${option.datasetKey}_${option.columnName} `}
109
- >
110
- {option.columnName}
111
- </option>
112
- ))}
113
- </select>
114
- </label>
115
- <label>
116
- <div className='edit-label column-heading mt-2'>
117
- Filter SubGrouping
118
- <span></span>
119
- </div>
120
- <select
121
- value={subGrouping?.columnName ?? ''}
122
- onChange={e => {
123
- handleSubGroupColumnNameChange(e.target)
124
- }}
125
- >
126
- <option value=''>- Select Option -</option>
127
- {columnNameOptionsInDataset.map(option => {
128
- if (option.columnName !== filter.columnName) {
129
- return (
130
- <option
131
- value={option.columnName}
132
- data-set={option.datasetKey}
133
- key={`subFilter_${option.datasetKey}_${option.columnName} `}
134
- >
135
- {option.columnName}
136
- </option>
137
- )
138
- }
139
- })}
140
- </select>
141
- </label>
192
+ <Select
193
+ label='Filter Grouping'
194
+ value={
195
+ filter.columnName
196
+ ? `${filter.columnName}|${
197
+ columnNameOptionsInDataset.find(opt => opt.columnName === filter.columnName)?.datasetKey || ''
198
+ }`
199
+ : ''
200
+ }
201
+ options={[
202
+ { value: '', label: '- Select Option -' },
203
+ ...columnNameOptionsInDataset.map(option => ({
204
+ value: `${option.columnName}|${option.datasetKey}`,
205
+ label: option.columnName
206
+ }))
207
+ ]}
208
+ onChange={e => handleFitlerGroupColumnNameChange(e.target.value)}
209
+ />
210
+ <Select
211
+ label='Filter SubGrouping'
212
+ value={
213
+ subGrouping?.columnName
214
+ ? `${subGrouping.columnName}|${
215
+ columnNameOptionsInDataset.find(opt => opt.columnName === subGrouping.columnName)?.datasetKey || ''
216
+ }`
217
+ : ''
218
+ }
219
+ options={[
220
+ { value: '', label: '- Select Option -' },
221
+ ...columnNameOptionsInDataset
222
+ .filter(option => option.columnName !== filter.columnName)
223
+ .map(option => ({
224
+ value: `${option.columnName}|${option.datasetKey}`,
225
+ label: option.columnName
226
+ }))
227
+ ]}
228
+ onChange={e => handleSubGroupColumnNameChange(e.target.value)}
229
+ />
142
230
 
143
231
  {/* Default Value for Main Group */}
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
  }
@@ -1,4 +1,4 @@
1
- import { useContext, useState } from 'react'
1
+ import { useContext, useState, useRef } from 'react'
2
2
  import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
3
3
  import Filters from './DashboardFilters'
4
4
  import { changeFilterActive } from '../../helpers/changeFilterActive'
@@ -12,6 +12,7 @@ import DashboardFiltersEditor from './DashboardFiltersEditor'
12
12
  import { ViewPort } from '@cdc/core/types/ViewPort'
13
13
  import { hasDashboardApplyBehavior } from '../../helpers/hasDashboardApplyBehavior'
14
14
  import * as apiFilterHelpers from '../../helpers/apiFilterHelpers'
15
+ import * as filterResetHelpers from '../../helpers/filterResetHelpers'
15
16
  import { applyQueuedActive } from '@cdc/core/components/Filters/helpers/applyQueuedActive'
16
17
  import './dashboardfilter.styles.css'
17
18
  import { updateChildFilters } from '../../helpers/updateChildFilters'
@@ -49,9 +50,15 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
49
50
  const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns, setAPILoading } = state
50
51
  const dispatch = useContext(DashboardDispatchContext)
51
52
 
53
+ // Track filter version to prevent stale async updates from overwriting cleared filters
54
+ const filterVersionRef = useRef(0)
55
+
52
56
  const applyFilters = e => {
53
57
  e.preventDefault() // prevent form submission
54
58
 
59
+ // Increment version to invalidate any pending async filter operations from handleOnChange
60
+ filterVersionRef.current += 1
61
+
55
62
  const dashboardConfig = {
56
63
  ...state.config.dashboard,
57
64
  sharedFilters: [...state.config.dashboard.sharedFilters] // Only clone the array we need to modify
@@ -59,18 +66,21 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
59
66
 
60
67
  const nonAutoLoadFilterIndexes = Object.values(state.config.visualizations)
61
68
  .filter(v => v.type === 'dashboardFilters')
62
- .reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, viz.sharedFilterIndexes] : acc), [])
69
+ .reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, ...viz.sharedFilterIndexes] : acc), [])
63
70
  const allRequiredFiltersSelected = !dashboardConfig.sharedFilters.some((filter, filterIndex) => {
64
71
  if (nonAutoLoadFilterIndexes.includes(filterIndex)) {
65
- return !filter.active && !filter.queuedActive
72
+ const activeValue = filter.queuedActive || filter.active
73
+ // Check if filter is not selected OR is set to its reset label
74
+ const isNotSelected = !activeValue || (filter.resetLabel && activeValue === filter.resetLabel)
75
+ return isNotSelected
66
76
  } else {
67
77
  // autoload filters don't need to be selected to apply filters
68
78
  return false
69
79
  }
70
80
  })
71
81
  if (allRequiredFiltersSelected) {
72
- if (hasDashboardApplyBehavior(state.config.visualizations)) {
73
- dispatch({ type: 'SET_FILTERS_APPLIED', payload: true })
82
+ const hasApplyBehavior = hasDashboardApplyBehavior(state.config.visualizations)
83
+ if (hasApplyBehavior) {
74
84
  const queryParams = getQueryParams()
75
85
  let needsQueryUpdate = false
76
86
  dashboardConfig.sharedFilters.forEach(sharedFilter => {
@@ -93,32 +103,108 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
93
103
  setAPILoading(true)
94
104
  dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
95
105
 
96
- // Clear data when applying filters to force fresh reload
97
- const emptyData = Object.keys(state.data).reduce((acc, key) => {
98
- acc[key] = []
99
- return acc
100
- }, {})
106
+ // Capture current version for this operation
107
+ const operationVersion = filterVersionRef.current
108
+ const isStale = () => filterVersionRef.current !== operationVersion
101
109
 
102
- const emptyFilteredData = Object.keys(state.filteredData).reduce((acc, key) => {
103
- acc[key] = []
104
- return acc
105
- }, {})
110
+ loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns, undefined, undefined, isStale)
111
+ .then(async newFilters => {
112
+ // Skip if operation is stale
113
+ if (isStale()) {
114
+ return
115
+ }
106
116
 
107
- dispatch({ type: 'SET_DATA', payload: emptyData })
108
- dispatch({ type: 'SET_FILTERED_DATA', payload: emptyFilteredData })
117
+ // First try to reload URL data (for filters that actually change the API call)
118
+ await reloadURLData(newFilters)
109
119
 
110
- loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns)
111
- .then(newFilters => {
112
- reloadURLData(newFilters)
120
+ // Set filters applied AFTER data is loaded to prevent "no data" flash
121
+ if (hasApplyBehavior) {
122
+ dispatch({ type: 'SET_FILTERS_APPLIED', payload: true })
123
+ }
124
+ setAPILoading(false)
113
125
  })
114
126
  .catch(e => {
115
127
  console.error(e)
128
+ setAPILoading(false)
116
129
  })
117
130
  } else {
118
131
  // TODO noftify of required fields
119
132
  }
120
133
  }
121
134
 
135
+ const handleReset = e => {
136
+ e.preventDefault()
137
+
138
+ // Increment version to invalidate any pending async filter operations
139
+ filterVersionRef.current += 1
140
+
141
+ const dashboardConfig = {
142
+ ...state.config.dashboard,
143
+ sharedFilters: _.cloneDeep(state.config.dashboard.sharedFilters)
144
+ }
145
+
146
+ const queryParams = getQueryParams()
147
+ let needsQueryUpdate = false
148
+
149
+ // Reset each filter to empty/resetLabel state (forceEmpty = true)
150
+ dashboardConfig.sharedFilters.forEach((filter, i) => {
151
+ const resetValue = filterResetHelpers.getFilterResetValue(filter, apiFilterDropdowns, true)
152
+ filterResetHelpers.resetFilterToValue(dashboardConfig.sharedFilters[i], resetValue, apiFilterDropdowns)
153
+
154
+ // Update query parameters if needed
155
+ if (
156
+ filter.setByQueryParameter &&
157
+ queryParams[filter.setByQueryParameter] !== dashboardConfig.sharedFilters[i].active
158
+ ) {
159
+ queryParams[filter.setByQueryParameter] = dashboardConfig.sharedFilters[i].active
160
+ needsQueryUpdate = true
161
+ }
162
+ })
163
+
164
+ if (needsQueryUpdate) {
165
+ updateQueryString(queryParams)
166
+ }
167
+
168
+ // Clear dropdown cache for child filters that depend on parents
169
+ const updatedDropdowns = filterResetHelpers.clearChildFilterDropdowns(
170
+ dashboardConfig.sharedFilters,
171
+ apiFilterDropdowns
172
+ )
173
+ setAPIFilterDropdowns(updatedDropdowns)
174
+
175
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
176
+
177
+ // Reset filtersApplied state to false when clearing filters
178
+ dispatch({ type: 'SET_FILTERS_APPLIED', payload: false })
179
+
180
+ // Update child filter values before filtering data
181
+ const updatedFilters = updateChildFilters(dashboardConfig.sharedFilters, state.data)
182
+
183
+ // Update filtered data immediately after resetting filters
184
+ // Use the updated filters instead of state
185
+ const clonedState = {
186
+ ...state,
187
+ config: {
188
+ ...state.config,
189
+ dashboard: {
190
+ ...state.config.dashboard,
191
+ sharedFilters: updatedFilters
192
+ }
193
+ }
194
+ }
195
+ const newFilteredData = getFilteredData(clonedState)
196
+ dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
197
+
198
+ publishAnalyticsEvent({
199
+ vizType: dashboardConfig.type,
200
+ vizSubType: getVizSubType(dashboardConfig),
201
+ eventType: `dashboard_filter_reset`,
202
+ eventAction: 'click',
203
+ eventLabel: interactionLabel,
204
+ vizTitle: getVizTitle(dashboardConfig)
205
+ })
206
+ }
207
+
122
208
  const handleOnChange = (index: number, value: string | string[]) => {
123
209
  const newConfig = {
124
210
  ...dashboardConfig,
@@ -157,12 +243,18 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
157
243
  apiFilterDropdowns,
158
244
  changedFilterIndexes
159
245
  )
246
+ // Capture current version for this operation
247
+ const operationVersion = filterVersionRef.current
248
+ const isStale = () => filterVersionRef.current !== operationVersion
249
+
160
250
  if (isAutoSelectFilter && !missingFilterSelections) {
161
251
  // a dropdown has been selected that doesn't
162
252
  // require the Go Button
163
253
  setAPIFilterDropdowns(loadingFilterMemo)
164
- loadAPIFilters(newSharedFilters, loadingFilterMemo).then(filters => {
165
- reloadURLData(filters)
254
+ loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale).then(filters => {
255
+ if (!isStale()) {
256
+ reloadURLData(filters)
257
+ }
166
258
  })
167
259
  } else {
168
260
  newSharedFilters[index].queuedActive = value
@@ -170,11 +262,12 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
170
262
  // Don't clear data immediately - keep existing data until new data loads
171
263
  // Only update the filter dropdowns and prepare for reload
172
264
  setAPIFilterDropdowns(loadingFilterMemo)
173
- loadAPIFilters(newSharedFilters, loadingFilterMemo)
265
+ loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale)
174
266
  }
175
267
  } else {
268
+ const updatedFilters = updateChildFilters(newSharedFilters, state.data)
176
269
  if (newSharedFilters[index].type === 'urlfilter' && newSharedFilters[index].apiFilter) {
177
- reloadURLData(newSharedFilters)
270
+ reloadURLData(updatedFilters)
178
271
  } else {
179
272
  const clonedState = {
180
273
  ...state,
@@ -182,13 +275,13 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
182
275
  ...state.config,
183
276
  dashboard: {
184
277
  ...state.config.dashboard,
185
- sharedFilters: newSharedFilters
278
+ sharedFilters: updatedFilters
186
279
  }
187
280
  }
188
281
  }
189
282
  const newFilteredData = getFilteredData(clonedState)
190
283
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
191
- dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
284
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: updatedFilters })
192
285
  }
193
286
  }
194
287
  }
@@ -204,9 +297,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
204
297
  // if all of the filters are hidden filters don't display the VisualizationWrapper
205
298
  const filters = visualizationConfig?.sharedFilterIndexes
206
299
  ?.map(Number)
207
- .map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
300
+ ?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
208
301
 
209
- const displayNone = filters.length ? filters.every(filter => filter.showDropdown === false) : false
302
+ const displayNone = filters?.length ? filters.every(filter => filter.showDropdown === false) : false
210
303
  if (displayNone && !isEditor) return <></>
211
304
  return (
212
305
  <Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
@@ -224,8 +317,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
224
317
  {!displayNone && (
225
318
  <Layout.Responsive isEditor={isEditor}>
226
319
  <div
227
- className={`${isEditor ? ' is-editor' : ''
228
- } cove-component__content col-12 cove-dashboard-filters-container`}
320
+ className={`${
321
+ isEditor ? ' is-editor' : ''
322
+ } cove-component__content col-12 cove-dashboard-filters-container`}
229
323
  >
230
324
  <Filters
231
325
  show={visualizationConfig?.sharedFilterIndexes?.map(Number)}
@@ -235,6 +329,12 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
235
329
  showSubmit={visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad}
236
330
  applyFilters={applyFilters}
237
331
  applyFiltersButtonText={visualizationConfig.applyFiltersButtonText}
332
+ handleReset={
333
+ visualizationConfig.filterBehavior === FilterBehavior.Apply &&
334
+ (visualizationConfig.showClearButton ?? true)
335
+ ? handleReset
336
+ : undefined
337
+ }
238
338
  />
239
339
  </div>
240
340
  </Layout.Responsive>
@@ -1,9 +1,17 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import DashboardFilters from '../DashboardFilters'
3
+ import '../../../scss/main.scss'
3
4
 
4
5
  const meta: Meta<typeof DashboardFilters> = {
5
6
  title: 'Components/Atoms/Inputs/DashboardFilters',
6
- component: DashboardFilters
7
+ component: DashboardFilters,
8
+ decorators: [
9
+ Story => (
10
+ <div className='cdc-open-viz-module type-dashboard'>
11
+ <Story />
12
+ </div>
13
+ )
14
+ ]
7
15
  }
8
16
 
9
17
  type Story = StoryObj<typeof DashboardFilters>
@@ -11,11 +19,62 @@ type Story = StoryObj<typeof DashboardFilters>
11
19
  export const Example_1: Story = {
12
20
  args: {
13
21
  filters: [
14
- { type: 'datafilter', key: 'label here', values: [1, 2, 3, 4] },
15
- { type: 'datafilter', key: 'something' }
22
+ {
23
+ type: 'datafilter',
24
+ key: 'label here',
25
+ values: ['1', '2', '3', '4'],
26
+ columnName: 'label',
27
+ showDropdown: true,
28
+ id: 0,
29
+ parents: []
30
+ } as any,
31
+ {
32
+ type: 'datafilter',
33
+ key: 'something',
34
+ values: ['A', 'B', 'C'],
35
+ columnName: 'something',
36
+ showDropdown: true,
37
+ id: 1,
38
+ parents: []
39
+ } as any
16
40
  ],
41
+ show: [0, 1],
42
+ apiFilterDropdowns: {},
17
43
  handleOnChange: () => {}
18
44
  }
19
45
  }
20
46
 
47
+ export const WithClearButton: Story = {
48
+ args: {
49
+ filters: [
50
+ {
51
+ type: 'datafilter',
52
+ key: 'Category',
53
+ values: ['Option 1', 'Option 2', 'Option 3'],
54
+ active: 'Option 1',
55
+ columnName: 'category',
56
+ showDropdown: true,
57
+ id: 0,
58
+ parents: []
59
+ } as any,
60
+ {
61
+ type: 'datafilter',
62
+ key: 'Status',
63
+ values: ['Active', 'Inactive', 'Pending'],
64
+ active: 'Active',
65
+ columnName: 'status',
66
+ showDropdown: true,
67
+ id: 1,
68
+ parents: []
69
+ } as any
70
+ ],
71
+ show: [0, 1],
72
+ apiFilterDropdowns: {},
73
+ handleOnChange: () => {},
74
+ showSubmit: true,
75
+ applyFilters: () => {},
76
+ handleReset: () => {}
77
+ }
78
+ }
79
+
21
80
  export default meta