@cdc/dashboard 4.25.10 → 4.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dynamic_Data.md +66 -0
- package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
- package/dist/cdcdashboard.js +84214 -79641
- package/examples/api-dashboard-data.json +272 -0
- package/examples/api-dashboard-years.json +11 -0
- package/examples/api-geographies-data.json +11 -0
- package/examples/api-test/categories.json +18 -0
- package/examples/api-test/chart-data.json +602 -0
- package/examples/api-test/topics.json +47 -0
- package/examples/api-test/years.json +22 -0
- package/examples/markup-axis-label.json +4167 -0
- package/examples/private/big-dashboard.json +39095 -39077
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/chronic-dash.json +1584 -0
- package/examples/private/clade-2.json +430 -0
- package/examples/private/diabetes.json +546 -196
- package/examples/private/map-issue.json +2260 -0
- package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
- package/examples/private/mpinc-state-reports.json +2260 -0
- package/examples/private/mpox.json +38128 -0
- package/examples/private/nwss/rsv.json +1240 -0
- package/examples/private/reset.json +32920 -0
- package/examples/private/simple-dash.json +490 -0
- package/examples/private/test-dash.json +0 -0
- package/examples/private/test123.json +491 -0
- package/examples/test-api-filter-reset.json +132 -0
- package/examples/test-dashboard-simple.json +503 -0
- package/index.html +25 -26
- package/package.json +11 -11
- package/src/CdcDashboardComponent.tsx +35 -10
- package/src/DashboardContext.tsx +3 -1
- package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
- package/src/_stories/Dashboard.stories.tsx +402 -1
- package/src/_stories/_mock/custom-order-new-values.json +116 -0
- package/src/_stories/_mock/filter-cascade.json +3350 -0
- package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
- package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
- package/src/_stories/_mock/parent-child-filters.json +233 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +54 -31
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +118 -50
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +96 -108
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +196 -59
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +129 -29
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +62 -3
- package/src/components/DataDesignerModal.tsx +18 -6
- package/src/components/Header/Header.tsx +53 -21
- package/src/components/Toggle/Toggle.tsx +48 -48
- package/src/components/VisualizationRow.tsx +73 -6
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
- package/src/components/Widget/Widget.tsx +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +24 -6
- package/src/helpers/apiFilterHelpers.ts +26 -2
- package/src/helpers/changeFilterActive.ts +67 -65
- package/src/helpers/filterData.ts +52 -7
- package/src/helpers/filterResetHelpers.ts +102 -0
- package/src/helpers/formatConfigBeforeSave.ts +6 -5
- package/src/helpers/getUpdateConfig.ts +91 -91
- package/src/helpers/getVizConfig.ts +2 -2
- package/src/helpers/loadAPIFilters.ts +109 -99
- package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
- package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
- package/src/helpers/updateChildFilters.ts +50 -27
- package/src/index.tsx +1 -0
- package/src/scss/editor-panel.scss +3 -431
- package/src/scss/main.scss +142 -25
- package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
- package/src/test/CdcDashboard.test.jsx +9 -4
- package/src/types/Dashboard.ts +1 -0
- package/src/types/DashboardFilters.ts +9 -8
- package/src/types/FilterStyles.ts +8 -7
- package/src/types/SharedFilter.ts +13 -0
- package/LICENSE +0 -201
- package/examples/private/DEV-11072.json +0 -7591
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_qaly_data.csv +0 -18721
- package/examples/private/burden_toolkit_mortality_yll_data.csv +0 -18721
- package/examples/private/pedro.json +0 -1
- package/src/helpers/getAutoLoadVisualization.ts +0 -11
- package/src/scss/mixins.scss +0 -47
- package/src/scss/variables.scss +0 -5
- /package/dist/{cdcdashboard-548642e6.es.js → cdcdashboard-Ct2SB0vL.es.js} +0 -0
|
@@ -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 =
|
|
51
|
-
|
|
52
|
-
|
|
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 =
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
)
|
|
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:
|
|
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
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
//
|
|
97
|
-
const
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
117
|
+
// First try to reload URL data (for filters that actually change the API call)
|
|
118
|
+
await reloadURLData(newFilters)
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
300
|
+
?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
|
|
208
301
|
|
|
209
|
-
const displayNone = filters
|
|
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={`${
|
|
228
|
-
|
|
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
|
-
{
|
|
15
|
-
|
|
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
|