@cdc/dashboard 4.25.8 → 4.25.11

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 (88) hide show
  1. package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
  2. package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
  3. package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
  4. package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
  5. package/dist/cdcdashboard.js +80040 -75976
  6. package/examples/api-test/categories.json +18 -0
  7. package/examples/api-test/chart-data.json +602 -0
  8. package/examples/api-test/topics.json +47 -0
  9. package/examples/api-test/years.json +22 -0
  10. package/examples/markup-axis-label.json +4167 -0
  11. package/examples/private/DEV-10538.json +407 -0
  12. package/examples/private/DEV-11405.json +39112 -0
  13. package/examples/private/big-dashboard.json +39112 -0
  14. package/examples/private/brfs-2.json +1532 -0
  15. package/examples/private/brfs.json +2128 -2138
  16. package/examples/private/clade-2.json +430 -0
  17. package/examples/private/delete.json +32919 -0
  18. package/examples/private/diabetes.json +5582 -0
  19. package/examples/private/example-2.json +49796 -0
  20. package/examples/private/group-legend-test.json +328 -0
  21. package/examples/private/map.json +1211 -0
  22. package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +14041 -0
  23. package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +14041 -0
  24. package/examples/private/markup-footer/burden_toolkit_mortality_qaly_data.csv +18721 -0
  25. package/examples/private/markup-footer/burden_toolkit_mortality_yll_data.csv +18721 -0
  26. package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
  27. package/examples/private/markup-variables.json +1451 -0
  28. package/examples/private/markup.json +5471 -0
  29. package/examples/private/mpox.json +38128 -0
  30. package/examples/private/north-dakota.json +1132 -0
  31. package/examples/private/ophdst.json +38754 -0
  32. package/examples/private/pedro.json +1 -0
  33. package/examples/private/pivot.json +683 -0
  34. package/examples/private/reset.json +32920 -0
  35. package/examples/private/sewershed.json +435 -0
  36. package/examples/private/tobacco.json +1938 -0
  37. package/examples/test-api-filter-reset.json +132 -0
  38. package/index.html +2 -2
  39. package/package.json +16 -10
  40. package/src/CdcDashboard.tsx +1 -3
  41. package/src/CdcDashboardComponent.tsx +34 -16
  42. package/src/DashboardContext.tsx +5 -1
  43. package/src/_stories/Dashboard.API.stories.tsx +62 -0
  44. package/src/_stories/Dashboard.stories.tsx +492 -472
  45. package/src/_stories/_mock/api/cessation.json +1 -0
  46. package/src/_stories/_mock/api/data-explorer.json +1 -0
  47. package/src/_stories/_mock/api/explore-by-location.json +1 -0
  48. package/src/_stories/_mock/api/explore-by-topic.json +1 -0
  49. package/src/_stories/_mock/api/legislation.json +1 -0
  50. package/src/_stories/_mock/api/oral-health-data.json +1 -0
  51. package/src/_stories/_mock/custom-order-new-values.json +116 -0
  52. package/src/components/CollapsibleVisualizationRow.tsx +1 -1
  53. package/src/components/DashboardFilters/DashboardFilters.tsx +34 -23
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +29 -12
  55. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +81 -112
  56. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +82 -52
  57. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +130 -31
  58. package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +80 -21
  59. package/src/components/DataDesignerModal.tsx +227 -210
  60. package/src/components/Header/Header.tsx +13 -12
  61. package/src/components/Toggle/Toggle.tsx +48 -47
  62. package/src/components/VisualizationRow.tsx +13 -6
  63. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -0
  64. package/src/components/Widget/Widget.tsx +47 -18
  65. package/src/helpers/addValuesToDashboardFilters.ts +111 -60
  66. package/src/helpers/apiFilterHelpers.ts +190 -166
  67. package/src/helpers/filterData.ts +52 -7
  68. package/src/helpers/filterResetHelpers.ts +102 -0
  69. package/src/helpers/formatConfigBeforeSave.ts +137 -0
  70. package/src/helpers/getVizConfig.ts +36 -18
  71. package/src/helpers/loadAPIFilters.ts +109 -99
  72. package/src/helpers/reloadURLHelpers.ts +1 -1
  73. package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
  74. package/src/helpers/tests/formatConfigBeforeSave.test.ts +69 -0
  75. package/src/index.tsx +1 -1
  76. package/src/scss/editor-panel.scss +3 -431
  77. package/src/scss/grid.scss +7 -5
  78. package/src/scss/main.scss +1 -24
  79. package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
  80. package/src/types/DashboardFilters.ts +9 -8
  81. package/src/types/InitialState.ts +12 -12
  82. package/vite.config.js +1 -1
  83. package/vitest.config.ts +16 -0
  84. package/src/coreStyles_dashboard.scss +0 -3
  85. package/src/helpers/getAutoLoadVisualization.ts +0 -11
  86. package/src/scss/mixins.scss +0 -47
  87. package/src/scss/variables.scss +0 -5
  88. /package/dist/{cdcdashboard-548642e6.es.js → cdcdashboard-Ct2SB0vL.es.js} +0 -0
@@ -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,10 +12,12 @@ 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'
18
19
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
20
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
19
21
 
20
22
  type SubOptions = { subOptions?: Record<'value' | 'text', string>[] }
21
23
 
@@ -48,9 +50,15 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
48
50
  const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns, setAPILoading } = state
49
51
  const dispatch = useContext(DashboardDispatchContext)
50
52
 
53
+ // Track filter version to prevent stale async updates from overwriting cleared filters
54
+ const filterVersionRef = useRef(0)
55
+
51
56
  const applyFilters = e => {
52
57
  e.preventDefault() // prevent form submission
53
58
 
59
+ // Increment version to invalidate any pending async filter operations from handleOnChange
60
+ filterVersionRef.current += 1
61
+
54
62
  const dashboardConfig = {
55
63
  ...state.config.dashboard,
56
64
  sharedFilters: [...state.config.dashboard.sharedFilters] // Only clone the array we need to modify
@@ -58,18 +66,21 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
58
66
 
59
67
  const nonAutoLoadFilterIndexes = Object.values(state.config.visualizations)
60
68
  .filter(v => v.type === 'dashboardFilters')
61
- .reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, viz.sharedFilterIndexes] : acc), [])
69
+ .reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, ...viz.sharedFilterIndexes] : acc), [])
62
70
  const allRequiredFiltersSelected = !dashboardConfig.sharedFilters.some((filter, filterIndex) => {
63
71
  if (nonAutoLoadFilterIndexes.includes(filterIndex)) {
64
- 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
65
76
  } else {
66
77
  // autoload filters don't need to be selected to apply filters
67
78
  return false
68
79
  }
69
80
  })
70
81
  if (allRequiredFiltersSelected) {
71
- if (hasDashboardApplyBehavior(state.config.visualizations)) {
72
- dispatch({ type: 'SET_FILTERS_APPLIED', payload: true })
82
+ const hasApplyBehavior = hasDashboardApplyBehavior(state.config.visualizations)
83
+ if (hasApplyBehavior) {
73
84
  const queryParams = getQueryParams()
74
85
  let needsQueryUpdate = false
75
86
  dashboardConfig.sharedFilters.forEach(sharedFilter => {
@@ -92,32 +103,105 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
92
103
  setAPILoading(true)
93
104
  dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
94
105
 
95
- // Clear data when applying filters to force fresh reload
96
- const emptyData = Object.keys(state.data).reduce((acc, key) => {
97
- acc[key] = []
98
- return acc
99
- }, {})
100
-
101
- const emptyFilteredData = Object.keys(state.filteredData).reduce((acc, key) => {
102
- acc[key] = []
103
- return acc
104
- }, {})
105
-
106
- dispatch({ type: 'SET_DATA', payload: emptyData })
107
- dispatch({ type: 'SET_FILTERED_DATA', payload: emptyFilteredData })
108
-
109
- loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns)
110
- .then(newFilters => {
111
- reloadURLData(newFilters)
106
+ // Capture current version for this operation
107
+ const operationVersion = filterVersionRef.current
108
+ const isStale = () => filterVersionRef.current !== operationVersion
109
+
110
+ loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns, undefined, undefined, isStale)
111
+ .then(async newFilters => {
112
+ // Skip if operation is stale
113
+ if (isStale()) {
114
+ return
115
+ }
116
+
117
+ // First try to reload URL data (for filters that actually change the API call)
118
+ await reloadURLData(newFilters)
119
+
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)
112
125
  })
113
126
  .catch(e => {
114
127
  console.error(e)
128
+ setAPILoading(false)
115
129
  })
116
130
  } else {
117
131
  // TODO noftify of required fields
118
132
  }
119
133
  }
120
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 filtered data immediately after resetting filters
181
+ // Use the updated dashboardConfig filters instead of state
182
+ const clonedState = {
183
+ ...state,
184
+ config: {
185
+ ...state.config,
186
+ dashboard: {
187
+ ...state.config.dashboard,
188
+ sharedFilters: dashboardConfig.sharedFilters
189
+ }
190
+ }
191
+ }
192
+ const newFilteredData = getFilteredData(clonedState)
193
+ dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
194
+
195
+ publishAnalyticsEvent({
196
+ vizType: dashboardConfig.type,
197
+ vizSubType: getVizSubType(dashboardConfig),
198
+ eventType: `dashboard_filter_reset`,
199
+ eventAction: 'click',
200
+ eventLabel: interactionLabel,
201
+ vizTitle: getVizTitle(dashboardConfig)
202
+ })
203
+ }
204
+
121
205
  const handleOnChange = (index: number, value: string | string[]) => {
122
206
  const newConfig = {
123
207
  ...dashboardConfig,
@@ -134,12 +218,15 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
134
218
  visualizationConfig
135
219
  )
136
220
 
137
- publishAnalyticsEvent(
138
- 'dashboard_filter_changed',
139
- 'change',
140
- `${interactionLabel}|key_${newSharedFilters?.[index]?.key}|value_${value}`,
141
- 'dashboard'
142
- )
221
+ publishAnalyticsEvent({
222
+ vizType: dashboardConfig.type,
223
+ vizSubType: getVizSubType(dashboardConfig),
224
+ eventType: `dashboard_filter_changed`,
225
+ eventAction: 'change',
226
+ eventLabel: `${interactionLabel}`,
227
+ vizTitle: getVizTitle(dashboardConfig),
228
+ specifics: `key: ${newConfig.dashboard.sharedFilters[index]?.columnName || 'unknown'}, value: ${value}`
229
+ })
143
230
 
144
231
  // sets the active filter option that the user just selected.
145
232
  dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
@@ -153,12 +240,18 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
153
240
  apiFilterDropdowns,
154
241
  changedFilterIndexes
155
242
  )
243
+ // Capture current version for this operation
244
+ const operationVersion = filterVersionRef.current
245
+ const isStale = () => filterVersionRef.current !== operationVersion
246
+
156
247
  if (isAutoSelectFilter && !missingFilterSelections) {
157
248
  // a dropdown has been selected that doesn't
158
249
  // require the Go Button
159
250
  setAPIFilterDropdowns(loadingFilterMemo)
160
- loadAPIFilters(newSharedFilters, loadingFilterMemo).then(filters => {
161
- reloadURLData(filters)
251
+ loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale).then(filters => {
252
+ if (!isStale()) {
253
+ reloadURLData(filters)
254
+ }
162
255
  })
163
256
  } else {
164
257
  newSharedFilters[index].queuedActive = value
@@ -166,7 +259,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
166
259
  // Don't clear data immediately - keep existing data until new data loads
167
260
  // Only update the filter dropdowns and prepare for reload
168
261
  setAPIFilterDropdowns(loadingFilterMemo)
169
- loadAPIFilters(newSharedFilters, loadingFilterMemo)
262
+ loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale)
170
263
  }
171
264
  } else {
172
265
  if (newSharedFilters[index].type === 'urlfilter' && newSharedFilters[index].apiFilter) {
@@ -232,6 +325,12 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
232
325
  showSubmit={visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad}
233
326
  applyFilters={applyFilters}
234
327
  applyFiltersButtonText={visualizationConfig.applyFiltersButtonText}
328
+ handleReset={
329
+ visualizationConfig.filterBehavior === FilterBehavior.Apply &&
330
+ (visualizationConfig.showClearButton ?? true)
331
+ ? handleReset
332
+ : undefined
333
+ }
235
334
  />
236
335
  </div>
237
336
  </Layout.Responsive>
@@ -1,21 +1,80 @@
1
- import { Meta, StoryObj } from '@storybook/react'
2
- import DashboardFilters from '../DashboardFilters'
3
-
4
- const meta: Meta<typeof DashboardFilters> = {
5
- title: 'Components/Atoms/Inputs/DashboardFilters',
6
- component: DashboardFilters
7
- }
8
-
9
- type Story = StoryObj<typeof DashboardFilters>
10
-
11
- export const Example_1: Story = {
12
- args: {
13
- filters: [
14
- { type: 'datafilter', key: 'label here', values: [1, 2, 3, 4] },
15
- { type: 'datafilter', key: 'something' }
16
- ],
17
- handleOnChange: () => {}
18
- }
19
- }
20
-
21
- export default meta
1
+ import { Meta, StoryObj } from '@storybook/react-vite'
2
+ import DashboardFilters from '../DashboardFilters'
3
+ import '../../../scss/main.scss'
4
+
5
+ const meta: Meta<typeof DashboardFilters> = {
6
+ title: 'Components/Atoms/Inputs/DashboardFilters',
7
+ component: DashboardFilters,
8
+ decorators: [
9
+ Story => (
10
+ <div className='cdc-open-viz-module type-dashboard'>
11
+ <Story />
12
+ </div>
13
+ )
14
+ ]
15
+ }
16
+
17
+ type Story = StoryObj<typeof DashboardFilters>
18
+
19
+ export const Example_1: Story = {
20
+ args: {
21
+ filters: [
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
40
+ ],
41
+ show: [0, 1],
42
+ apiFilterDropdowns: {},
43
+ handleOnChange: () => {}
44
+ }
45
+ }
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
+
80
+ export default meta