@cdc/dashboard 4.24.11 → 4.24.12-2
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/dist/cdcdashboard.js +49442 -49230
- package/examples/ed-visits-county-file.json +141 -357
- package/examples/private/DEV-10120.json +1294 -0
- package/examples/private/DEV-9199.json +606 -0
- package/examples/private/DEV-9684.json +2135 -0
- package/examples/private/DEV-9989.json +229 -0
- package/examples/private/art-dashboard.json +18174 -0
- package/examples/private/art-scratch.json +2406 -0
- package/examples/private/dashboard-config-ehdi.json +29915 -0
- package/examples/private/dashboard-margins.js +15 -0
- package/examples/private/dataset.json +1452 -0
- package/examples/private/ehdi-data.json +29502 -0
- package/examples/private/gaza-issue.json +1214 -0
- package/examples/private/workforce.json +2041 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +8 -15
- package/src/CdcDashboardComponent.tsx +53 -38
- package/src/DashboardContext.tsx +2 -0
- package/src/components/CollapsibleVisualizationRow.tsx +8 -2
- package/src/components/DashboardFilters/DashboardFilters.tsx +107 -59
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +2 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +54 -50
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +6 -2
- package/src/components/DashboardFilters/dashboardfilter.styles.css +16 -0
- package/src/components/VisualizationRow.tsx +30 -20
- package/src/components/Widget.tsx +1 -1
- package/src/data/initial-state.js +2 -1
- package/src/helpers/addValuesToDashboardFilters.ts +4 -2
- package/src/helpers/apiFilterHelpers.ts +55 -20
- package/src/helpers/changeFilterActive.ts +3 -0
- package/src/helpers/filterData.ts +1 -1
- package/src/helpers/loadAPIFilters.ts +25 -8
- package/src/helpers/reloadURLHelpers.ts +9 -2
- package/src/helpers/shouldLoadAllFilters.ts +30 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +85 -4
- package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +8 -3
- package/src/helpers/tests/reloadURLHelpers.test.ts +11 -5
- package/src/helpers/tests/shouldLoadAllFilters.test.ts +117 -0
- package/src/store/dashboard.reducer.ts +2 -1
|
@@ -41,11 +41,10 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
|
|
|
41
41
|
if (viz.type === 'dashboardFilters') return false
|
|
42
42
|
const vizName = viz.general?.title || viz.title || vizKey
|
|
43
43
|
nameLookup[vizKey] = vizName
|
|
44
|
-
const notAdded = !filter.usedBy || filter.usedBy.indexOf(vizKey) === -1
|
|
45
44
|
const usesSharedFilter = viz.usesSharedFilter
|
|
46
45
|
const rowIndex = vizLookup.row
|
|
47
46
|
const dataConfiguredOnRow = config.rows[rowIndex].dataKey
|
|
48
|
-
return filter.setBy !== vizKey &&
|
|
47
|
+
return filter.setBy !== vizKey && !usesSharedFilter && !dataConfiguredOnRow
|
|
49
48
|
})
|
|
50
49
|
const rowOptions: number[] = []
|
|
51
50
|
|
|
@@ -363,20 +362,23 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
|
|
|
363
362
|
{isNestedDropDown && <APIInputs isSubgroup={true} />}
|
|
364
363
|
|
|
365
364
|
{!!parentFilters.length && (
|
|
366
|
-
<
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
365
|
+
<label>
|
|
366
|
+
<span className='edit-label column-heading mt-1'>Parent Filter(s): </span>
|
|
367
|
+
<MultiSelect
|
|
368
|
+
label='Parent Filter(s): '
|
|
369
|
+
options={parentFilters.map(key => ({ value: key, label: key }))}
|
|
370
|
+
fieldName='parents'
|
|
371
|
+
selected={filter.parents}
|
|
372
|
+
updateField={(_section, _subsection, _fieldname, newItems) => {
|
|
373
|
+
updateFilterProp('parents', newItems)
|
|
374
|
+
}}
|
|
375
|
+
/>
|
|
376
|
+
</label>
|
|
375
377
|
)}
|
|
376
378
|
|
|
377
|
-
<
|
|
378
|
-
|
|
379
|
-
|
|
379
|
+
<label>
|
|
380
|
+
<span className='edit-label column-heading mt-1'>
|
|
381
|
+
Used By: (optional)
|
|
380
382
|
<Tooltip style={{ textTransform: 'none' }}>
|
|
381
383
|
<Tooltip.Target>
|
|
382
384
|
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
@@ -388,17 +390,19 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
|
|
|
388
390
|
</p>
|
|
389
391
|
</Tooltip.Content>
|
|
390
392
|
</Tooltip>
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
393
|
+
</span>
|
|
394
|
+
<MultiSelect
|
|
395
|
+
options={usedByOptions.map(opt => ({
|
|
396
|
+
value: opt,
|
|
397
|
+
label: usedByNameLookup[opt]
|
|
398
|
+
}))}
|
|
399
|
+
fieldName='usedBy'
|
|
400
|
+
selected={filter.usedBy}
|
|
401
|
+
updateField={(_section, _subsection, _fieldname, newItems) => {
|
|
402
|
+
updateFilterProp('usedBy', newItems)
|
|
403
|
+
}}
|
|
404
|
+
/>
|
|
405
|
+
</label>
|
|
402
406
|
|
|
403
407
|
<TextField
|
|
404
408
|
label='Reset Label: '
|
|
@@ -473,31 +477,31 @@ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilte
|
|
|
473
477
|
</select>
|
|
474
478
|
</label>
|
|
475
479
|
<label>
|
|
476
|
-
<span className='edit-label column-heading'>
|
|
477
|
-
|
|
478
|
-
{
|
|
479
|
-
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
480
|
+
<span className='edit-label column-heading mt-1'>
|
|
481
|
+
Used By: (optional)
|
|
482
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
483
|
+
<Tooltip.Target>
|
|
484
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
485
|
+
</Tooltip.Target>
|
|
486
|
+
<Tooltip.Content>
|
|
487
|
+
<p>
|
|
488
|
+
Select if you would like specific visualizations or rows to use this filter. Otherwise the
|
|
489
|
+
filter will be added to all api requests.
|
|
490
|
+
</p>
|
|
491
|
+
</Tooltip.Content>
|
|
492
|
+
</Tooltip>
|
|
493
|
+
</span>
|
|
494
|
+
<MultiSelect
|
|
495
|
+
options={usedByOptions.map(opt => ({
|
|
496
|
+
value: opt,
|
|
497
|
+
label: usedByNameLookup[opt]
|
|
498
|
+
}))}
|
|
499
|
+
fieldName='usedBy'
|
|
500
|
+
selected={filter.usedBy}
|
|
501
|
+
updateField={(_section, _subsection, _fieldname, newItems) => {
|
|
502
|
+
updateFilterProp('usedBy', newItems)
|
|
503
|
+
}}
|
|
504
|
+
/>
|
|
501
505
|
</label>
|
|
502
506
|
<TextField
|
|
503
507
|
label='Reset Label: '
|
|
@@ -31,6 +31,7 @@ type DashboardFiltersProps = {
|
|
|
31
31
|
isEditor?: boolean
|
|
32
32
|
setConfig: (config: DashboardFilters) => void
|
|
33
33
|
currentViewport?: ViewPort
|
|
34
|
+
setAPILoading: (loading: boolean) => void
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
@@ -41,7 +42,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
41
42
|
isEditor = false
|
|
42
43
|
}) => {
|
|
43
44
|
const state = useContext(DashboardContext)
|
|
44
|
-
const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns } = state
|
|
45
|
+
const { config: dashboardConfig, reloadURLData, loadAPIFilters, setAPIFilterDropdowns, setAPILoading } = state
|
|
45
46
|
const dispatch = useContext(DashboardDispatchContext)
|
|
46
47
|
|
|
47
48
|
const applyFilters = e => {
|
|
@@ -81,7 +82,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
81
82
|
updateQueryString(queryParams)
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
+
setAPILoading(true)
|
|
85
86
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
|
|
86
87
|
dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(_.cloneDeep(state)) })
|
|
87
88
|
loadAPIFilters(dashboardConfig.sharedFilters, apiFilterDropdowns)
|
|
@@ -105,6 +106,9 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
|
|
|
105
106
|
visualizationConfig
|
|
106
107
|
)
|
|
107
108
|
|
|
109
|
+
// sets the active filter option that the user just selected.
|
|
110
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
|
|
111
|
+
|
|
108
112
|
if (hasDashboardApplyBehavior(dashboardConfig.visualizations)) {
|
|
109
113
|
const isAutoSelectFilter = visualizationConfig.autoLoad
|
|
110
114
|
const missingFilterSelections = newConfig.dashboard.sharedFilters.some(f => !f.active)
|
|
@@ -8,4 +8,20 @@
|
|
|
8
8
|
height: calc(1.5em + 0.75rem + 2px);
|
|
9
9
|
align-self: flex-end;
|
|
10
10
|
}
|
|
11
|
+
.loading-filter {
|
|
12
|
+
position: relative;
|
|
13
|
+
.spinner-border {
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 55%;
|
|
16
|
+
right: 10%;
|
|
17
|
+
width: 1.5rem;
|
|
18
|
+
height: 1.5rem;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
:is(select):disabled {
|
|
22
|
+
background-color: var(--lightestGray);
|
|
23
|
+
& > :is(option) {
|
|
24
|
+
color: var(--darkGray);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
11
27
|
}
|
|
@@ -24,6 +24,7 @@ type VisualizationWrapperProps = {
|
|
|
24
24
|
children: React.ReactNode
|
|
25
25
|
currentViewport: ViewPort
|
|
26
26
|
groupName: string
|
|
27
|
+
hideVisualization: boolean
|
|
27
28
|
row: ConfigRow
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -31,10 +32,13 @@ const VisualizationWrapper: React.FC<VisualizationWrapperProps> = ({
|
|
|
31
32
|
allExpanded,
|
|
32
33
|
currentViewport,
|
|
33
34
|
groupName,
|
|
35
|
+
hideVisualization,
|
|
34
36
|
row,
|
|
35
37
|
children
|
|
36
38
|
}) => {
|
|
37
|
-
return
|
|
39
|
+
return hideVisualization ? (
|
|
40
|
+
<></>
|
|
41
|
+
) : row.expandCollapseAllButtons ? (
|
|
38
42
|
<div className='collapsable-multiviz-container'>
|
|
39
43
|
<CollapsibleVisualizationRow
|
|
40
44
|
allExpanded={allExpanded}
|
|
@@ -47,7 +51,7 @@ const VisualizationWrapper: React.FC<VisualizationWrapperProps> = ({
|
|
|
47
51
|
</div>
|
|
48
52
|
) : (
|
|
49
53
|
<>
|
|
50
|
-
<h3>{groupName}</h3>
|
|
54
|
+
{groupName !== '' ? <h3>{groupName}</h3> : <></>}
|
|
51
55
|
{children}
|
|
52
56
|
</>
|
|
53
57
|
)
|
|
@@ -59,6 +63,7 @@ type VizRowProps = {
|
|
|
59
63
|
groupName: string
|
|
60
64
|
row: ConfigRow
|
|
61
65
|
rowIndex: number
|
|
66
|
+
inNoDataState: boolean
|
|
62
67
|
setSharedFilter: Function
|
|
63
68
|
updateChildConfig: Function
|
|
64
69
|
apiFilterDropdowns: APIFilterDropdowns
|
|
@@ -71,6 +76,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
71
76
|
groupName,
|
|
72
77
|
row,
|
|
73
78
|
rowIndex: index,
|
|
79
|
+
inNoDataState,
|
|
74
80
|
setSharedFilter,
|
|
75
81
|
updateChildConfig,
|
|
76
82
|
apiFilterDropdowns,
|
|
@@ -81,11 +87,6 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
81
87
|
const setToggled = (colIndex: number) => {
|
|
82
88
|
setShow(show.map((_, i) => i === colIndex))
|
|
83
89
|
}
|
|
84
|
-
const inNoDataState = useMemo(() => {
|
|
85
|
-
const vals = Object.values(rawData).flatMap(val => val)
|
|
86
|
-
if (!vals.length) return true
|
|
87
|
-
return vals.some(val => val === undefined)
|
|
88
|
-
}, [rawData])
|
|
89
90
|
|
|
90
91
|
const footnotesConfig = useMemo(() => {
|
|
91
92
|
if (row.footnotesId) {
|
|
@@ -95,7 +96,10 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
95
96
|
// the multiViz filtering filtering is applied after the dashboard filters
|
|
96
97
|
const categoryFootnote = footnoteConfig.formattedData.filter(d => d[row.multiVizColumn] === vizCategory)
|
|
97
98
|
footnoteConfig.formattedData = categoryFootnote
|
|
99
|
+
} else {
|
|
100
|
+
footnoteConfig.formattedData = dashboardFilteredData[row.footnotesId]
|
|
98
101
|
}
|
|
102
|
+
|
|
99
103
|
return footnoteConfig
|
|
100
104
|
}
|
|
101
105
|
return null
|
|
@@ -120,13 +124,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
120
124
|
return false
|
|
121
125
|
}
|
|
122
126
|
return (
|
|
123
|
-
<div
|
|
124
|
-
className={`row mb-5 ${row.equalHeight ? 'equal-height' : ''} ${row.toggle ? 'toggle' : ''}`}
|
|
125
|
-
key={`row__${index}`}
|
|
126
|
-
>
|
|
127
|
-
{row.toggle && (
|
|
128
|
-
<Toggle row={row} visualizations={config.visualizations} active={show.indexOf(true)} setToggled={setToggled} />
|
|
129
|
-
)}
|
|
127
|
+
<div className={`row${row.equalHeight ? ' equal-height' : ''}${row.toggle ? ' toggle' : ''}`} key={`row__${index}`}>
|
|
130
128
|
{row.columns.map((col, colIndex) => {
|
|
131
129
|
if (col.width) {
|
|
132
130
|
if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`col col-${col.width}`}></div>
|
|
@@ -150,22 +148,34 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
150
148
|
{visualizationConfig.dataKey} (Go to Table)
|
|
151
149
|
</a>
|
|
152
150
|
)
|
|
153
|
-
|
|
151
|
+
|
|
152
|
+
const hideVisualization =
|
|
154
153
|
inNoDataState &&
|
|
155
|
-
visualizationConfig.
|
|
156
|
-
applyButtonNotClicked(visualizationConfig)
|
|
154
|
+
visualizationConfig.filterBehavior !== 'Apply Button' &&
|
|
155
|
+
(visualizationConfig.type !== 'dashboardFilters' || applyButtonNotClicked(visualizationConfig))
|
|
157
156
|
|
|
158
157
|
const shouldShow = row.toggle === undefined || (row.toggle && show[colIndex])
|
|
159
158
|
|
|
160
159
|
return (
|
|
161
160
|
<div
|
|
162
161
|
key={`vis__${index}__${colIndex}`}
|
|
163
|
-
className={`
|
|
162
|
+
className={`col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${
|
|
163
|
+
hideVisualization ? ' hide-parent-visualization' : ' mt-5 p-1'
|
|
164
|
+
}`}
|
|
164
165
|
>
|
|
166
|
+
{row.toggle && !hideVisualization && (
|
|
167
|
+
<Toggle
|
|
168
|
+
row={row}
|
|
169
|
+
visualizations={config.visualizations}
|
|
170
|
+
active={show.indexOf(true)}
|
|
171
|
+
setToggled={setToggled}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
165
174
|
<VisualizationWrapper
|
|
166
175
|
allExpanded={allExpanded}
|
|
167
176
|
currentViewport={currentViewport}
|
|
168
177
|
groupName={groupName}
|
|
178
|
+
hideVisualization={hideVisualization}
|
|
169
179
|
row={row}
|
|
170
180
|
>
|
|
171
181
|
{visualizationConfig.type === 'chart' && (
|
|
@@ -272,7 +282,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
272
282
|
configUrl={undefined}
|
|
273
283
|
/>
|
|
274
284
|
)}
|
|
275
|
-
{visualizationConfig.type === 'dashboardFilters' &&
|
|
285
|
+
{visualizationConfig.type === 'dashboardFilters' && (
|
|
276
286
|
<DashboardSharedFilters
|
|
277
287
|
setConfig={newConfig => {
|
|
278
288
|
updateChildConfig(col.widget, newConfig)
|
|
@@ -308,7 +318,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
308
318
|
}
|
|
309
319
|
return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
|
|
310
320
|
})}
|
|
311
|
-
{row.footnotesId ? (
|
|
321
|
+
{row.footnotesId && !inNoDataState ? (
|
|
312
322
|
<FootnotesStandAlone
|
|
313
323
|
isEditor={false}
|
|
314
324
|
visualizationKey={row.footnotesId}
|
|
@@ -90,7 +90,7 @@ const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
|
|
|
90
90
|
const loadSampleData = () => {
|
|
91
91
|
const dataKey = config.rows[widgetConfig.rowIdx]?.dataKey || widgetConfig?.dataKey
|
|
92
92
|
const dataset = config.datasets[dataKey]
|
|
93
|
-
const _data = data[dataset
|
|
93
|
+
const _data = data[dataset?.dataUrl]
|
|
94
94
|
if (_data && !_data.length) {
|
|
95
95
|
const url = changeDataLimit(dataset.dataUrl, 100)
|
|
96
96
|
fetchRemoteData(url).then(responseData => {
|
|
@@ -30,9 +30,11 @@ const getSelector = (filter: SharedFilter) => {
|
|
|
30
30
|
|
|
31
31
|
export const addValuesToDashboardFilters = (
|
|
32
32
|
filters: SharedFilter[],
|
|
33
|
-
data: Record<string, any[]
|
|
33
|
+
data: Record<string, any[]>,
|
|
34
|
+
filtersToSkip: number[] = []
|
|
34
35
|
): Array<SharedFilter> => {
|
|
35
|
-
return filters?.map(filter => {
|
|
36
|
+
return filters?.map((filter, index) => {
|
|
37
|
+
if (filtersToSkip.includes(index)) return filter
|
|
36
38
|
if (filter.type === 'urlfilter') return filter
|
|
37
39
|
const filterCopy = _.cloneDeep(filter)
|
|
38
40
|
const filterValues = generateValuesForFilter(getSelector(filter), data)
|
|
@@ -3,7 +3,7 @@ import { APIFilterDropdowns, DropdownOptions } from '../components/DashboardFilt
|
|
|
3
3
|
import { APIFilter } from '../types/APIFilter'
|
|
4
4
|
import { SharedFilter } from '../types/SharedFilter'
|
|
5
5
|
import _ from 'lodash'
|
|
6
|
-
import {
|
|
6
|
+
import { getQueryParam } from '@cdc/core/helpers/queryStringUtils'
|
|
7
7
|
import { FILTER_STYLE } from '../types/FilterStyles'
|
|
8
8
|
|
|
9
9
|
/** key for the dropdowns object */
|
|
@@ -17,10 +17,10 @@ export const getLoadingFilterMemo = (
|
|
|
17
17
|
apiFiltersEndpoints.reduce((acc, endpoint, currIndex) => {
|
|
18
18
|
const _key: DropdownsKey = endpoint
|
|
19
19
|
const hasChanged = changedChildFilterIndexes.includes(currIndex)
|
|
20
|
-
if (apiFilterDropdowns[_key]
|
|
20
|
+
if (apiFilterDropdowns[_key] && !hasChanged) {
|
|
21
21
|
acc[_key] = apiFilterDropdowns[_key]
|
|
22
22
|
} else {
|
|
23
|
-
acc[_key] =
|
|
23
|
+
acc[_key] = undefined
|
|
24
24
|
}
|
|
25
25
|
return acc
|
|
26
26
|
}, {})
|
|
@@ -37,7 +37,7 @@ export const getParentParams = (
|
|
|
37
37
|
const key = filter.apiFilter.valueSelector || ''
|
|
38
38
|
const subKey = filter.apiFilter.subgroupValueSelector || ''
|
|
39
39
|
const val = filter.queuedActive ? filter.queuedActive[0] : (filter.active as string) || ''
|
|
40
|
-
const subVal = filter.queuedActive ? filter.queuedActive[1] : filter.subGrouping
|
|
40
|
+
const subVal = filter.queuedActive ? filter.queuedActive[1] : filter.subGrouping?.active || ''
|
|
41
41
|
return [
|
|
42
42
|
{ key, value: val },
|
|
43
43
|
{ key: subKey, value: subVal }
|
|
@@ -53,6 +53,8 @@ export const getParentParams = (
|
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export const notAllParentsSelected = parentParams => parentParams?.some(({ value }) => value === '')
|
|
57
|
+
|
|
56
58
|
export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
|
|
57
59
|
const { textSelector, valueSelector, subgroupTextSelector, subgroupValueSelector } = apiFilter
|
|
58
60
|
if (subgroupValueSelector) {
|
|
@@ -83,9 +85,8 @@ export const getToFetch = (
|
|
|
83
85
|
const _key = baseEndpoint
|
|
84
86
|
if (apiFilterDropdowns[_key]) return // don't reload cached filter
|
|
85
87
|
const parentParams = getParentParams(filter, sharedFilters)
|
|
86
|
-
const notAllParentsSelected = parentParams?.some(({ value }) => value === '')
|
|
87
88
|
|
|
88
|
-
if (notAllParentsSelected) return // don't send request for dependent children filter options
|
|
89
|
+
if (notAllParentsSelected(parentParams)) return // don't send request for dependent children filter options
|
|
89
90
|
|
|
90
91
|
const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
|
|
91
92
|
toFetch[endpoint] = [_key, index]
|
|
@@ -93,6 +94,38 @@ export const getToFetch = (
|
|
|
93
94
|
return toFetch
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
export const setActiveNestedDropdown = (dropdownOptions, sharedFilter) => {
|
|
98
|
+
const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
|
|
99
|
+
const defaultValue = dropdownOptions[0]?.value
|
|
100
|
+
const subDefaultValue = dropdownOptions[0]?.subOptions[0].value
|
|
101
|
+
const subDefaultQueryParamValue = getQueryParam(sharedFilter?.subGrouping.setByQueryParameter)
|
|
102
|
+
if (!sharedFilter.active) {
|
|
103
|
+
sharedFilter.active = defaultQueryParamValue || defaultValue
|
|
104
|
+
sharedFilter.subGrouping.active = subDefaultQueryParamValue || subDefaultValue
|
|
105
|
+
} else {
|
|
106
|
+
const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
|
|
107
|
+
sharedFilter.active = currentOption ? currentOption.value : defaultValue
|
|
108
|
+
if (currentOption) {
|
|
109
|
+
const currentSubOption = currentOption.subOptions.find(option => option.value === sharedFilter.subGrouping.active)
|
|
110
|
+
sharedFilter.subGrouping.active = currentSubOption?.value || subDefaultValue
|
|
111
|
+
} else {
|
|
112
|
+
sharedFilter.subGrouping.active = subDefaultValue
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const setActiveMultiDropdown = (dropdownOptions, sharedFilter) => {
|
|
118
|
+
const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
|
|
119
|
+
const multiDefaultQueryParamValue = Array.isArray(defaultQueryParamValue)
|
|
120
|
+
? defaultQueryParamValue
|
|
121
|
+
: defaultQueryParamValue?.split(',')
|
|
122
|
+
const multiDefaultValue = defaultQueryParamValue ? multiDefaultQueryParamValue : [dropdownOptions[0]?.value]
|
|
123
|
+
const currentOption = ((sharedFilter.active as string[]) || []).filter(activeVal =>
|
|
124
|
+
dropdownOptions.find(option => option.value === activeVal)
|
|
125
|
+
)
|
|
126
|
+
sharedFilter.active = currentOption.length ? currentOption : multiDefaultValue
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
export const setAutoLoadDefaultValue = (
|
|
97
130
|
sharedFilterIndex: number,
|
|
98
131
|
dropdownOptions: DropdownOptions,
|
|
@@ -102,24 +135,26 @@ export const setAutoLoadDefaultValue = (
|
|
|
102
135
|
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
103
136
|
const sharedFilter = _.cloneDeep(sharedFiltersCopy[sharedFilterIndex])
|
|
104
137
|
if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) return sharedFilter // no autoLoading happening
|
|
105
|
-
|
|
138
|
+
const hasQueryParameter = sharedFilter.setByQueryParameter
|
|
139
|
+
? Boolean(getQueryParam(sharedFilter.setByQueryParameter))
|
|
140
|
+
: false
|
|
141
|
+
if (autoLoadFilterIndexes.includes(sharedFilterIndex) || hasQueryParameter) {
|
|
106
142
|
const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
|
|
107
143
|
const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
|
|
108
144
|
if (filterParents && notAllParentFiltersSelected) return sharedFilter
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
const defaultQueryParamValue = queryParams[sharedFilter?.setByQueryParameter]
|
|
114
|
-
sharedFilter.active = defaultQueryParamValue || defaultValue
|
|
115
|
-
} else if (sharedFilter.filterStyle === FILTER_STYLE.multiSelect) {
|
|
116
|
-
const currentOption = (sharedFilter.active as string[]).filter(activeVal =>
|
|
117
|
-
dropdownOptions.find(option => option.value === activeVal)
|
|
118
|
-
)
|
|
119
|
-
sharedFilter.active = currentOption.length ? currentOption : defaultValue
|
|
145
|
+
if (sharedFilter.filterStyle === FILTER_STYLE.multiSelect) {
|
|
146
|
+
setActiveMultiDropdown(dropdownOptions, sharedFilter)
|
|
147
|
+
} else if (sharedFilter.filterStyle === FILTER_STYLE.nestedDropdown) {
|
|
148
|
+
setActiveNestedDropdown(dropdownOptions, sharedFilter)
|
|
120
149
|
} else {
|
|
121
|
-
const
|
|
122
|
-
|
|
150
|
+
const defaultValue = dropdownOptions[0]?.value
|
|
151
|
+
const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
|
|
152
|
+
if (!sharedFilter.active) {
|
|
153
|
+
sharedFilter.active = defaultQueryParamValue || defaultValue
|
|
154
|
+
} else {
|
|
155
|
+
const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
|
|
156
|
+
sharedFilter.active = currentOption ? currentOption.value : defaultValue
|
|
157
|
+
}
|
|
123
158
|
}
|
|
124
159
|
}
|
|
125
160
|
return sharedFilter
|
|
@@ -13,6 +13,9 @@ const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
|
|
|
13
13
|
if (childFilterIndexes.length) {
|
|
14
14
|
childFilterIndexes.forEach(filterIndex => {
|
|
15
15
|
sharedFilters[filterIndex].active = ''
|
|
16
|
+
if (sharedFilters[filterIndex].subGrouping) {
|
|
17
|
+
sharedFilters[filterIndex].subGrouping.active = ''
|
|
18
|
+
}
|
|
16
19
|
})
|
|
17
20
|
}
|
|
18
21
|
return childFilterIndexes
|
|
@@ -24,7 +24,7 @@ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function filter(data = [], filters: SharedFilter[], condition) {
|
|
27
|
-
const activeFilters =
|
|
27
|
+
const activeFilters = _.filter(filters, f => (f.resetLabel === f.active ? f.values?.includes(f.resetLabel) : true))
|
|
28
28
|
return data.filter(row => {
|
|
29
29
|
const foundMatchingFilter = activeFilters.find(filter => {
|
|
30
30
|
const currentValue = row[filter.columnName]
|
|
@@ -3,6 +3,7 @@ import { APIFilterDropdowns } from '../components/DashboardFilters'
|
|
|
3
3
|
import { SharedFilter } from '../types/SharedFilter'
|
|
4
4
|
import * as apiFilterHelpers from './apiFilterHelpers'
|
|
5
5
|
import { APIFilter } from '../types/APIFilter'
|
|
6
|
+
import { getParentParams, notAllParentsSelected } from './apiFilterHelpers'
|
|
6
7
|
|
|
7
8
|
export const loadAPIFiltersFactory = (
|
|
8
9
|
dispatch: Function,
|
|
@@ -13,20 +14,30 @@ export const loadAPIFiltersFactory = (
|
|
|
13
14
|
const loadAPIFilters = (
|
|
14
15
|
sharedFilters: SharedFilter[],
|
|
15
16
|
dropdowns: APIFilterDropdowns,
|
|
16
|
-
|
|
17
|
+
loadAll?: boolean,
|
|
18
|
+
recursiveLimit = 50
|
|
17
19
|
): Promise<SharedFilter[]> => {
|
|
18
20
|
if (!sharedFilters) return
|
|
21
|
+
const allIndexes = sharedFilters.map((_, index) => index)
|
|
22
|
+
const _autoLoadFilterIndexes = loadAll ? allIndexes : autoLoadFilterIndexes
|
|
19
23
|
sharedFilters = sharedFilters.map((filter, index) =>
|
|
20
24
|
apiFilterHelpers.setAutoLoadDefaultValue(
|
|
21
25
|
index,
|
|
22
26
|
dropdowns[filter.apiFilter?.apiEndpoint],
|
|
23
27
|
sharedFilters,
|
|
24
|
-
|
|
28
|
+
_autoLoadFilterIndexes
|
|
25
29
|
)
|
|
26
30
|
)
|
|
27
31
|
const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
|
|
28
32
|
const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
|
|
29
33
|
const toFetch = apiFilterHelpers.getToFetch(sharedFilters, dropdowns)
|
|
34
|
+
const loadingDropdowns = Object.values(toFetch).reduce(
|
|
35
|
+
(acc, [dropdownsKey]) => ({ ...acc, [dropdownsKey]: null }),
|
|
36
|
+
{}
|
|
37
|
+
)
|
|
38
|
+
setAPIFilterDropdowns(currentState => {
|
|
39
|
+
return { ...currentState, ...loadingDropdowns }
|
|
40
|
+
})
|
|
30
41
|
const newDropdowns = _.cloneDeep(dropdowns)
|
|
31
42
|
return Promise.all(
|
|
32
43
|
Object.keys(toFetch).map(
|
|
@@ -47,7 +58,7 @@ export const loadAPIFiltersFactory = (
|
|
|
47
58
|
index,
|
|
48
59
|
_filterValues,
|
|
49
60
|
sharedFilters,
|
|
50
|
-
|
|
61
|
+
_autoLoadFilterIndexes
|
|
51
62
|
)
|
|
52
63
|
sharedFilters[index] = newDefaultSelectedFilter
|
|
53
64
|
})
|
|
@@ -63,16 +74,22 @@ export const loadAPIFiltersFactory = (
|
|
|
63
74
|
})
|
|
64
75
|
)
|
|
65
76
|
).then(() => {
|
|
66
|
-
const
|
|
67
|
-
|
|
77
|
+
const toLoad = sharedFilters.reduce((acc, curr, index) => {
|
|
78
|
+
// the filter is autoloading and it hasn't finished yet
|
|
79
|
+
if (_autoLoadFilterIndexes.includes(index) && !curr.active) {
|
|
80
|
+
if (notAllParentsSelected(getParentParams(curr, sharedFilters))) {
|
|
81
|
+
return acc
|
|
82
|
+
}
|
|
83
|
+
return [...acc, index]
|
|
84
|
+
}
|
|
68
85
|
return acc
|
|
69
|
-
},
|
|
70
|
-
if (
|
|
86
|
+
}, [])
|
|
87
|
+
if (!toLoad.length || recursiveLimit === 0) {
|
|
71
88
|
setAPIFilterDropdowns(newDropdowns)
|
|
72
89
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
|
|
73
90
|
return sharedFilters
|
|
74
91
|
} else {
|
|
75
|
-
return loadAPIFilters(sharedFilters, newDropdowns, recursiveLimit - 1)
|
|
92
|
+
return loadAPIFilters(sharedFilters, newDropdowns, loadAll, recursiveLimit - 1)
|
|
76
93
|
}
|
|
77
94
|
})
|
|
78
95
|
}
|
|
@@ -4,6 +4,7 @@ import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
|
|
|
4
4
|
import { AnyVisualization, Visualization } from '@cdc/core/types/Visualization'
|
|
5
5
|
import _ from 'lodash'
|
|
6
6
|
import { DashboardConfig } from '../types/DashboardConfig'
|
|
7
|
+
import { ConfigRow } from '../types/ConfigRow'
|
|
7
8
|
|
|
8
9
|
export const isUpdateNeeded = (
|
|
9
10
|
filters: SharedFilter[],
|
|
@@ -104,9 +105,15 @@ export const getVisualizationsWithFormattedData = (visualizations: Record<string
|
|
|
104
105
|
export const filterUsedByDataUrl = (
|
|
105
106
|
filter: SharedFilter,
|
|
106
107
|
datasetKey: string,
|
|
107
|
-
visualizations: Record<string, AnyVisualization
|
|
108
|
+
visualizations: Record<string, AnyVisualization>,
|
|
109
|
+
rows: ConfigRow[]
|
|
108
110
|
) => {
|
|
109
111
|
if (!filter.usedBy || !filter.usedBy.length) return true
|
|
110
|
-
const vizUsingFilters = filter.usedBy?.map(
|
|
112
|
+
const vizUsingFilters = filter.usedBy?.map(vizOrRowKey => visualizations[vizOrRowKey] || rows[vizOrRowKey])
|
|
113
|
+
// push any footnotes which are using the filter also
|
|
114
|
+
filter.usedBy?.forEach(vizOrRowKey => {
|
|
115
|
+
if (rows[vizOrRowKey] && rows[vizOrRowKey].footnotesId)
|
|
116
|
+
return vizUsingFilters.push(visualizations[rows[vizOrRowKey].footnotesId])
|
|
117
|
+
})
|
|
111
118
|
return vizUsingFilters?.some(viz => viz?.dataKey === datasetKey)
|
|
112
119
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getQueryParam } from '@cdc/core/helpers/queryStringUtils'
|
|
2
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
|
|
4
|
+
export const shouldLoadAllFilters = (config, isEditorPanel): boolean => {
|
|
5
|
+
const autoLoad = Boolean(getQueryParam('cove-auto-load'))
|
|
6
|
+
const activeConfig = config.multiDashboards ? config.multiDashboards[config.activeDashboard] : config
|
|
7
|
+
const hasFilterByFileNameFunctionality = activeConfig.dashboard.sharedFilters?.some(
|
|
8
|
+
filter => filter.filterBy === 'File Name'
|
|
9
|
+
)
|
|
10
|
+
const isAutoLoadTab = Object.values(activeConfig.visualizations).reduce((acc, viz: Visualization) => {
|
|
11
|
+
if (acc === false) return acc
|
|
12
|
+
if (viz.visualizationType === 'dashboardFilters') {
|
|
13
|
+
if (viz.filterBehavior === 'Apply Button') return false
|
|
14
|
+
if (viz.autoLoad) {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return acc
|
|
19
|
+
}, undefined)
|
|
20
|
+
if (autoLoad || isAutoLoadTab || hasFilterByFileNameFunctionality || isEditorPanel) {
|
|
21
|
+
const rowDataSetKeys = activeConfig.rows.map(row => row.dataKey).filter(Boolean)
|
|
22
|
+
const dataKeys = Object.values(activeConfig.visualizations)
|
|
23
|
+
.map((visualization: Visualization) => visualization.dataKey)
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.concat(rowDataSetKeys)
|
|
26
|
+
const missingData = dataKeys.find(dataset => !config.datasets[dataset].data?.length)
|
|
27
|
+
return Boolean(missingData)
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
30
|
+
}
|