@cdc/dashboard 4.24.4 → 4.24.7

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 (75) hide show
  1. package/dist/cdcdashboard.js +179228 -141419
  2. package/examples/custom/css/respiratory.css +236 -0
  3. package/examples/custom/js/respiratory.js +242 -0
  4. package/examples/default-multi-dataset-shared-filter.json +1729 -0
  5. package/examples/ed-visits-county-file.json +618 -0
  6. package/examples/filtered-dash.json +6 -21
  7. package/index.html +12 -3
  8. package/package.json +12 -11
  9. package/src/CdcDashboard.tsx +5 -1
  10. package/src/CdcDashboardComponent.tsx +156 -334
  11. package/src/DashboardContext.tsx +9 -1
  12. package/src/_stories/Dashboard.stories.tsx +31 -3
  13. package/src/_stories/_mock/dashboard-gallery.json +534 -523
  14. package/src/_stories/_mock/markup-include.json +78 -0
  15. package/src/_stories/_mock/multi-dashboards.json +914 -0
  16. package/src/_stories/_mock/multi-viz.json +2 -3
  17. package/src/_stories/_mock/pivot-filter.json +15 -11
  18. package/src/_stories/_mock/standalone-table.json +2 -0
  19. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  20. package/src/components/Column.tsx +1 -1
  21. package/src/components/DashboardFilters/DashboardFilters.tsx +80 -0
  22. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  23. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  24. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +367 -0
  25. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  26. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -0
  27. package/src/components/DashboardFilters/index.ts +3 -0
  28. package/src/components/DataDesignerModal.tsx +9 -9
  29. package/src/components/ExpandCollapseButtons.tsx +20 -0
  30. package/src/components/Header/Header.tsx +1 -97
  31. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
  32. package/src/components/MultiConfigTabs/MultiTabs.tsx +3 -2
  33. package/src/components/Row.tsx +52 -19
  34. package/src/components/Toggle/Toggle.tsx +2 -4
  35. package/src/components/VisualizationRow.tsx +96 -29
  36. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  37. package/src/components/VisualizationsPanel/index.ts +1 -0
  38. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  39. package/src/components/Widget.tsx +26 -90
  40. package/src/helpers/apiFilterHelpers.ts +51 -0
  41. package/src/helpers/changeFilterActive.ts +30 -0
  42. package/src/helpers/filterData.ts +16 -56
  43. package/src/helpers/generateValuesForFilter.ts +1 -1
  44. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  45. package/src/helpers/getFilteredData.ts +4 -2
  46. package/src/helpers/getVizConfig.ts +23 -2
  47. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  48. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  49. package/src/helpers/iconHash.tsx +3 -3
  50. package/src/helpers/mapDataToConfig.ts +29 -0
  51. package/src/helpers/processData.ts +2 -3
  52. package/src/helpers/reloadURLHelpers.ts +68 -0
  53. package/src/helpers/tests/filterData.test.ts +1 -93
  54. package/src/scss/editor-panel.scss +1 -1
  55. package/src/scss/grid.scss +34 -27
  56. package/src/scss/main.scss +41 -3
  57. package/src/scss/variables.scss +4 -0
  58. package/src/store/dashboard.actions.ts +9 -10
  59. package/src/store/dashboard.reducer.ts +41 -13
  60. package/src/types/APIFilter.ts +1 -4
  61. package/src/types/ConfigRow.ts +2 -0
  62. package/src/types/Dashboard.ts +1 -1
  63. package/src/types/DashboardConfig.ts +2 -4
  64. package/src/types/DashboardFilters.ts +7 -0
  65. package/src/types/InitialState.ts +1 -1
  66. package/src/types/MultiDashboard.ts +2 -2
  67. package/src/types/SharedFilter.ts +2 -5
  68. package/src/types/Tab.ts +1 -1
  69. package/LICENSE +0 -201
  70. package/src/components/EditorWrapper/EditorWrapper.tsx +0 -52
  71. package/src/components/EditorWrapper/editor-wrapper.style.css +0 -13
  72. package/src/components/Filters.tsx +0 -88
  73. package/src/components/Header/FilterModal.tsx +0 -506
  74. package/src/components/VisualizationsPanel.tsx +0 -72
  75. package/src/helpers/getApiFilterKey.ts +0 -5
@@ -0,0 +1,367 @@
1
+ import _ from 'lodash'
2
+ import { APIFilter } from '../../../../types/APIFilter'
3
+ import { getVizRowColumnLocator } from '../../../../helpers/getVizRowColumnLocator'
4
+ import { TextField } from '@cdc/core/components/EditorPanel/Inputs'
5
+ import DataTransform from '@cdc/core/helpers/DataTransform'
6
+ import { useEffect, useMemo, useState } from 'react'
7
+ import { SharedFilter } from '../../../../types/SharedFilter'
8
+ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
9
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
10
+ import Icon from '@cdc/core/components/ui/Icon'
11
+ import MultiSelect from '@cdc/core/components/MultiSelect'
12
+ import { DashboardConfig } from '../../../../types/DashboardConfig'
13
+ import { Visualization } from '@cdc/core/types/Visualization'
14
+ import { hasDashboardApplyBehavior } from '../../../../helpers/hasDashboardApplyBehavior'
15
+
16
+ type FilterEditorProps = {
17
+ config: DashboardConfig
18
+ filter: SharedFilter
19
+ updateFilterProp: (name: keyof SharedFilter, value: any) => void
20
+ }
21
+
22
+ const FilterEditor: React.FC<FilterEditorProps> = ({ filter, config, updateFilterProp }) => {
23
+ const [columns, setColumns] = useState<string[]>([])
24
+ const transform = new DataTransform()
25
+
26
+ const parentFilters: string[] = (config.dashboard.sharedFilters || []).filter(({ key, type }) => key !== filter.key && type !== 'datafilter').map(({ key }) => key)
27
+
28
+ const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
29
+
30
+ const [usedByNameLookup, usedByOptions] = useMemo(() => {
31
+ const nameLookup = {}
32
+ const vizOptions = Object.keys(config.visualizations)
33
+ .filter(vizKey => {
34
+ const vizLookup = vizRowColumnLocator[vizKey]
35
+ if (!vizLookup) return false
36
+ const viz = config.visualizations[vizKey]
37
+ if (viz.type === 'dashboardFilters') return false
38
+ const notAdded = !filter.usedBy || filter.usedBy.indexOf(vizKey) === -1
39
+ const usesSharedFilter = viz.usesSharedFilter
40
+ const rowIndex = vizLookup.row
41
+ const dataConfiguredOnRow = config.rows[rowIndex].dataKey
42
+ return filter.setBy !== vizKey && notAdded && !usesSharedFilter && !dataConfiguredOnRow
43
+ })
44
+ .map(vizKey => {
45
+ const viz = config.visualizations[vizKey] as Visualization
46
+ const vizName = viz.general?.title || viz.title || vizKey
47
+ nameLookup[vizKey] = vizName
48
+ return vizKey
49
+ })
50
+ const rowOptions: number[] = []
51
+
52
+ config.rows.forEach((row, rowIndex) => {
53
+ if (!!row.dataKey) {
54
+ nameLookup[rowIndex] = `Row ${rowIndex + 1}`
55
+ rowOptions.push(rowIndex)
56
+ }
57
+ })
58
+
59
+ const rowsNotSelected = rowOptions.filter(row => !filter.usedBy || filter.usedBy.indexOf(row.toString()) === -1)
60
+ return [nameLookup, [...vizOptions, ...rowsNotSelected]]
61
+ }, [config.visualizations, filter.usedBy, filter.setBy, vizRowColumnLocator])
62
+
63
+ const loadColumnData = async () => {
64
+ const columns = {}
65
+ const dataKeys = Object.keys(config.datasets)
66
+
67
+ for (let i = 0; i < dataKeys.length; i++) {
68
+ const dataKey = dataKeys[i]
69
+ let _dataSet = config.datasets[dataKey]
70
+ if (!_dataSet.data && _dataSet.dataUrl) {
71
+ _dataSet = await fetchRemoteData(_dataSet.dataUrl)
72
+ if (_dataSet.dataDescription) {
73
+ try {
74
+ _dataSet = transform.autoStandardize(_dataSet.data)
75
+ _dataSet = transform.developerStandardize(_dataSet.data, _dataSet.dataDescription)
76
+ } catch (e) {
77
+ //Data not able to be standardized, leave as is
78
+ }
79
+ }
80
+ }
81
+
82
+ if (_dataSet.data) {
83
+ _dataSet.data.forEach(row => {
84
+ Object.keys(row).forEach(columnName => {
85
+ columns[columnName] = true
86
+ })
87
+ })
88
+ }
89
+ }
90
+
91
+ setColumns(Object.keys(columns))
92
+ }
93
+
94
+ useEffect(() => {
95
+ loadColumnData()
96
+ }, [config.datasets])
97
+
98
+ const addFilterUsedBy = (filter, value) => {
99
+ if (value === '') return
100
+ if (!filter.usedBy) filter.usedBy = []
101
+ filter.usedBy.push(value)
102
+ updateFilterProp('usedBy', filter.usedBy)
103
+ }
104
+
105
+ const removeFilterUsedBy = (filter, value) => {
106
+ let usedByIndex = filter.usedBy.indexOf(value)
107
+ if (usedByIndex !== -1) {
108
+ filter.usedBy.splice(usedByIndex, 1)
109
+ updateFilterProp('usedBy', filter.usedBy)
110
+ }
111
+ }
112
+
113
+ const updateAPIFilter = (key: keyof APIFilter, value: string | boolean) => {
114
+ const filterClone = _.cloneDeep(filter)
115
+ const _filter = filterClone.apiFilter || { apiEndpoint: '', valueSelector: '', textSelector: '' }
116
+ const newAPIFilter: APIFilter = { ..._filter, [key]: value }
117
+ updateFilterProp('apiFilter', newAPIFilter)
118
+ }
119
+
120
+ return (
121
+ <>
122
+ <label>
123
+ <span className='edit-label column-heading'>Filter Type: </span>
124
+ <select defaultValue={filter.type || ''} onChange={e => updateFilterProp('type', e.target.value)} disabled={!!filter.type}>
125
+ <option value=''>- Select Option -</option>
126
+ <option value='urlfilter'>URL</option>
127
+ <option value='datafilter'>Data</option>
128
+ </select>
129
+ </label>
130
+ {filter.type === 'urlfilter' && (
131
+ <>
132
+ <TextField label='Label' value={filter.key} updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)} />
133
+ {!hasDashboardApplyBehavior(config.visualizations) && (
134
+ <>
135
+ <label>
136
+ <span className='edit-label column-heading'>URL to Filter: </span>
137
+ <select defaultValue={filter.datasetKey || ''} onChange={e => updateFilterProp('datasetKey', e.target.value)}>
138
+ <option value=''>- Select Option -</option>
139
+ {Object.keys(config.datasets).map(datasetKey => {
140
+ if (config.datasets[datasetKey].dataUrl) {
141
+ return (
142
+ <option key={datasetKey} value={datasetKey}>
143
+ {config.datasets[datasetKey].dataUrl}
144
+ </option>
145
+ )
146
+ }
147
+ return null
148
+ })}
149
+ </select>
150
+ </label>
151
+ <label>
152
+ <span className='edit-label column-heading'>Filter By: </span>
153
+ <select defaultValue={filter.filterBy || ''} onChange={e => updateFilterProp('filterBy', e.target.value)}>
154
+ <option value=''>- Select Option -</option>
155
+ <option key={'query-string'} value={'Query String'}>
156
+ Query String
157
+ </option>
158
+ <option key={'file-name'} value={'File Name'}>
159
+ File Name
160
+ </option>
161
+ </select>
162
+ </label>
163
+ {filter.filterBy === 'File Name' && (
164
+ <>
165
+ <TextField
166
+ label='File Name: '
167
+ value={filter.fileName || ''}
168
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('fileName', value)}
169
+ tooltip={
170
+ <Tooltip style={{ textTransform: 'none' }}>
171
+ <Tooltip.Target>
172
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
173
+ </Tooltip.Target>
174
+ <Tooltip.Content>
175
+ <p>{`Add \${query}\ to replace the filename with the active dropdown value.`}</p>
176
+ </Tooltip.Content>
177
+ </Tooltip>
178
+ }
179
+ />
180
+
181
+ <label>
182
+ <span className='edit-label column-heading'>
183
+ White Space Replacments
184
+ <Tooltip style={{ textTransform: 'none' }}>
185
+ <Tooltip.Target>
186
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
187
+ </Tooltip.Target>
188
+ <Tooltip.Content>
189
+ <p>{`Set how whitespace characters will be handled in the file request`}</p>
190
+ </Tooltip.Content>
191
+ </Tooltip>
192
+ </span>
193
+ <select defaultValue={filter.whitespaceReplacement || 'Keep Spaces'} onChange={e => updateFilterProp('whitespaceReplacement', e.target.value)}>
194
+ <option key={'remove-spaces'} value={'Remove Spaces'}>
195
+ Remove Spaces
196
+ </option>
197
+ <option key={'replace-with-underscore'} value={'Replace With Underscore'}>
198
+ Replace With Underscore
199
+ </option>
200
+ <option key={'keep-spaces'} value={'Keep Spaces'}>
201
+ Keep Spaces
202
+ </option>
203
+ </select>
204
+ </label>
205
+ </>
206
+ )}
207
+ </>
208
+ )}
209
+ {filter.filterBy === 'Query String' && <TextField label='Query string parameter' value={filter.queryParameter} updateField={(_section, _subSection, _key, value) => updateFilterProp('queryParameter', value)} />}
210
+ <TextField label='Filter API Endpoint: ' value={filter.apiFilter?.apiEndpoint} updateField={(_section, _subSection, _key, value) => updateAPIFilter('apiEndpoint', value)} />
211
+ <TextField
212
+ label='Option Text Selector:'
213
+ value={filter.apiFilter?.textSelector}
214
+ updateField={(_section, _subSection, _key, value) => updateAPIFilter('textSelector', value)}
215
+ tooltip={
216
+ <>
217
+ <Tooltip style={{ textTransform: 'none' }}>
218
+ <Tooltip.Target>
219
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
220
+ </Tooltip.Target>
221
+ <Tooltip.Content>
222
+ <p>Text to use in the html option element. If none is applied value selector will be used.</p>
223
+ </Tooltip.Content>
224
+ </Tooltip>
225
+ {` * Optional`}
226
+ </>
227
+ }
228
+ />
229
+ <TextField
230
+ label='Option Value Selector:'
231
+ value={filter.apiFilter?.valueSelector}
232
+ updateField={(_section, _subSection, _key, value) => updateAPIFilter('valueSelector', value)}
233
+ tooltip={
234
+ <>
235
+ <Tooltip style={{ textTransform: 'none' }}>
236
+ <Tooltip.Target>
237
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
238
+ </Tooltip.Target>
239
+ <Tooltip.Content>
240
+ <p>Value to use in the html option element</p>
241
+ </Tooltip.Content>
242
+ </Tooltip>
243
+ {` * Required`}
244
+ </>
245
+ }
246
+ />
247
+
248
+ {!!parentFilters.length && (
249
+ <MultiSelect
250
+ label='Parent Filter(s): '
251
+ options={parentFilters.map(key => ({ value: key, label: key }))}
252
+ fieldName='parents'
253
+ selected={filter.parents}
254
+ updateField={(_section, _subsection, _fieldname, newItems) => {
255
+ updateFilterProp('parents', newItems)
256
+ }}
257
+ />
258
+ )}
259
+
260
+ <TextField label='Reset Label: ' value={filter.resetLabel || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)} />
261
+
262
+ <TextField label='Default Value Set By Query String Parameter: ' value={filter.setByQueryParameter || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)} />
263
+ </>
264
+ )}
265
+ {filter.type === 'datafilter' && (
266
+ <>
267
+ <label>
268
+ <span className='edit-label column-heading'>Filter: </span>
269
+ <select
270
+ value={filter.columnName}
271
+ onChange={e => {
272
+ updateFilterProp('columnName', e.target.value)
273
+ }}
274
+ >
275
+ <option value=''>- Select Option -</option>
276
+ {columns.map(dataKey => (
277
+ <option value={dataKey} key={`filter-column-select-item-${dataKey}`}>
278
+ {dataKey}
279
+ </option>
280
+ ))}
281
+ </select>
282
+ </label>
283
+
284
+ <TextField label='Label' value={filter.key} updateField={(_section, _subSection, _key, value) => updateFilterProp('key', value)} />
285
+
286
+ <label>
287
+ <span className='edit-label column-heading'>Show Dropdown</span>
288
+ <input
289
+ type='checkbox'
290
+ defaultChecked={filter.showDropdown === true}
291
+ onChange={e => {
292
+ updateFilterProp('showDropdown', !filter.showDropdown)
293
+ }}
294
+ />
295
+ </label>
296
+
297
+ <label>
298
+ <span className='edit-label column-heading'>Set By: </span>
299
+ <select value={filter.setBy} onChange={e => updateFilterProp('setBy', e.target.value)}>
300
+ <option value=''>- Select Option -</option>
301
+ {Object.keys(config.visualizations)
302
+ .filter(vizKey => config.visualizations[vizKey].type !== 'dashboardFilters')
303
+ .map(vizKey => {
304
+ const viz = config.visualizations[vizKey] as Visualization
305
+ return (
306
+ <option value={vizKey} key={`set-by-select-item-${vizKey}`}>
307
+ {viz.general?.title || viz.title || vizKey}
308
+ </option>
309
+ )
310
+ })}
311
+ </select>
312
+ </label>
313
+ <label>
314
+ <span className='edit-label column-heading'>Used By: </span>
315
+ <ul>
316
+ {filter.usedBy &&
317
+ filter.usedBy.map(opt => (
318
+ <li key={`used-by-list-item-${opt}`}>
319
+ <span>{usedByNameLookup[opt] || opt}</span>{' '}
320
+ <button
321
+ onClick={e => {
322
+ e.preventDefault()
323
+ removeFilterUsedBy(filter, opt)
324
+ }}
325
+ >
326
+ X
327
+ </button>
328
+ </li>
329
+ ))}
330
+ </ul>
331
+ <select value='' onChange={e => addFilterUsedBy(filter, e.target.value)}>
332
+ <option value=''>- Select Option -</option>
333
+ {usedByOptions.map(opt => (
334
+ <option value={opt} key={`used-by-select-item-${opt}`}>
335
+ {usedByNameLookup[opt] || opt}
336
+ </option>
337
+ ))}
338
+ </select>
339
+ </label>
340
+ <TextField label='Reset Label: ' value={filter.resetLabel || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('resetLabel', value)} />
341
+
342
+ <label>
343
+ <span className='edit-label column-heading'>Parent Filter: </span>
344
+ <select
345
+ value={filter.parents || []}
346
+ onChange={e => {
347
+ updateFilterProp('parents', e.target.value)
348
+ }}
349
+ >
350
+ <option value=''>Select a filter</option>
351
+ {config.dashboard.sharedFilters &&
352
+ config.dashboard.sharedFilters.map(sharedFilter => {
353
+ if (sharedFilter.key !== filter.key) {
354
+ return <option key={sharedFilter.key}>{sharedFilter.key}</option>
355
+ }
356
+ })}
357
+ </select>
358
+ </label>
359
+
360
+ <TextField label='Default Value Set By Query String Parameter: ' value={filter.setByQueryParameter || ''} updateField={(_section, _subSection, _key, value) => updateFilterProp('setByQueryParameter', value)} />
361
+ </>
362
+ )}
363
+ </>
364
+ )
365
+ }
366
+
367
+ export default FilterEditor
@@ -0,0 +1 @@
1
+ export { default } from './DashboardFiltersEditor'
@@ -0,0 +1,143 @@
1
+ import { useContext, useState } from 'react'
2
+ import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
3
+ import Filters from './DashboardFilters'
4
+ import { changeFilterActive } from '../../helpers/changeFilterActive'
5
+ import _ from 'lodash'
6
+ import { FilterBehavior } from '../Header/Header'
7
+ import { getFilteredData } from '../../helpers/getFilteredData'
8
+ import { DashboardFilters } from '../../types/DashboardFilters'
9
+ import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
10
+ import Layout from '@cdc/core/components/Layout'
11
+ import DashboardFiltersEditor from './DashboardFiltersEditor'
12
+ import { ViewPort } from '@cdc/core/types/ViewPort'
13
+ import { hasDashboardApplyBehavior } from '../../helpers/hasDashboardApplyBehavior'
14
+
15
+ export type DropdownOptions = Record<'value' | 'text', string>[]
16
+
17
+ export type APIFilterDropdowns = {
18
+ // null means still loading
19
+ [filtername: string]: null | DropdownOptions
20
+ }
21
+
22
+ type DashboardFiltersProps = {
23
+ apiFilterDropdowns: APIFilterDropdowns
24
+ visualizationConfig: DashboardFilters
25
+ isEditor?: boolean
26
+ setConfig: (config: DashboardFilters) => void
27
+ currentViewport?: ViewPort
28
+ }
29
+
30
+ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({ apiFilterDropdowns, visualizationConfig, setConfig: updateConfig, currentViewport, isEditor = false }) => {
31
+ const state = useContext(DashboardContext)
32
+ const { config: dashboardConfig, reloadURLData, loadAPIFilters } = state
33
+ const dispatch = useContext(DashboardDispatchContext)
34
+
35
+ const applyFilters = () => {
36
+ const dashboardConfig = _.cloneDeep(state.config.dashboard)
37
+ const nonAutoLoadFilterIndexes = Object.values(state.config.visualizations)
38
+ .filter(v => v.type === 'dashboardFilters')
39
+ .reduce((acc, viz: DashboardFilters) => (!viz.autoLoad ? [...acc, viz.sharedFilterIndexes] : acc), [])
40
+ const allRequiredFiltersSelected = !dashboardConfig.sharedFilters.some((filter, filterIndex) => {
41
+ if (nonAutoLoadFilterIndexes.includes(filterIndex)) {
42
+ return !filter.active && !filter.queuedActive
43
+ } else {
44
+ // autoload filters don't need to be selected to apply filters
45
+ return false
46
+ }
47
+ })
48
+ if (allRequiredFiltersSelected) {
49
+ if (hasDashboardApplyBehavior(state.config.visualizations)) {
50
+ const queryParams = getQueryParams()
51
+ let needsQueryUpdate = false
52
+ dashboardConfig.sharedFilters.forEach((sharedFilter, index) => {
53
+ if (sharedFilter.queuedActive) {
54
+ dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
55
+ delete dashboardConfig.sharedFilters[index].queuedActive
56
+
57
+ if (sharedFilter.setByQueryParameter && queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active) {
58
+ queryParams[sharedFilter.setByQueryParameter] = sharedFilter.active
59
+ needsQueryUpdate = true
60
+ }
61
+ }
62
+ })
63
+
64
+ if (needsQueryUpdate) {
65
+ updateQueryString(queryParams)
66
+ }
67
+ }
68
+
69
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
70
+ dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(_.cloneDeep(state)) })
71
+ loadAPIFilters(dashboardConfig.sharedFilters)
72
+ .then(newFilters => {
73
+ reloadURLData(newFilters)
74
+ })
75
+ .catch(e => {
76
+ console.error(e)
77
+ })
78
+ } else {
79
+ // TODO noftify of required fields
80
+ }
81
+ }
82
+
83
+ const handleOnChange = (index: number, value: string | string[]) => {
84
+ const newConfig = _.cloneDeep(dashboardConfig)
85
+ let newSharedFilters = changeFilterActive(index, value, newConfig.dashboard.sharedFilters, visualizationConfig)
86
+
87
+ if (hasDashboardApplyBehavior(dashboardConfig.visualizations)) {
88
+ const isAutoSelectFilter = visualizationConfig.autoLoad
89
+ const missingFilterSelections = newConfig.dashboard.sharedFilters.some(f => !f.active)
90
+ if (isAutoSelectFilter && !missingFilterSelections) {
91
+ // a dropdown has been selected that doesn't
92
+ // require the Go Button
93
+ loadAPIFilters(newSharedFilters).then(filters => {
94
+ reloadURLData(filters)
95
+ })
96
+ } else {
97
+ if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
98
+ newSharedFilters[index].queuedActive = value
99
+ // setData to empty object because we no longer have a data state.
100
+ dispatch({ type: 'SET_DATA', payload: {} })
101
+ dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
102
+ loadAPIFilters(newSharedFilters)
103
+ }
104
+ } else {
105
+ if (newSharedFilters[index].apiFilter) {
106
+ reloadURLData(newSharedFilters)
107
+ } else {
108
+ const clonedState = _.cloneDeep(state)
109
+ clonedState.config.dashboard.sharedFilters = newSharedFilters
110
+ const newFilteredData = getFilteredData(clonedState)
111
+ dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
112
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: newSharedFilters })
113
+ }
114
+ }
115
+ }
116
+ const [displayPanel, setDisplayPanel] = useState(true)
117
+ const onBackClick = () => {
118
+ setDisplayPanel(!displayPanel)
119
+ updateConfig({
120
+ ...visualizationConfig,
121
+ showEditorPanel: !displayPanel
122
+ })
123
+ }
124
+
125
+ return (
126
+ <Layout.VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
127
+ {isEditor && (
128
+ <Layout.Sidebar displayPanel={displayPanel} isDashboard={true} title={'Configure Dashboard Filters'} onBackClick={onBackClick}>
129
+ <DashboardFiltersEditor updateConfig={updateConfig} vizConfig={visualizationConfig} />
130
+ </Layout.Sidebar>
131
+ )}
132
+
133
+ <Layout.Responsive isEditor={isEditor}>
134
+ <div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''} cove-component__content col-12`}>
135
+ <Filters show={visualizationConfig?.sharedFilterIndexes?.map(Number)} filters={dashboardConfig.dashboard.sharedFilters || []} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
136
+ {visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad && <button onClick={applyFilters}>GO!</button>}
137
+ </div>
138
+ </Layout.Responsive>
139
+ </Layout.VisualizationWrapper>
140
+ )
141
+ }
142
+
143
+ export default DashboardFiltersWrapper
@@ -0,0 +1,3 @@
1
+ export { default } from './DashboardFiltersWrapper'
2
+
3
+ export type { APIFilterDropdowns, DropdownOptions } from './DashboardFiltersWrapper'
@@ -71,6 +71,11 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
71
71
  }
72
72
  }
73
73
 
74
+ const setExpandCollapseAllButtons = (selection: boolean) => {
75
+ dispatch({ type: 'UPDATE_ROW', payload: { rowIndex, rowData: { expandCollapseAllButtons: selection } } })
76
+ setCanContinue(true)
77
+ }
78
+
74
79
  return (
75
80
  <Modal>
76
81
  <Modal.Content>
@@ -121,15 +126,10 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
121
126
  }}
122
127
  />
123
128
  ) : (
124
- <InputSelect
125
- options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})}
126
- value={config.rows[rowIndex].multiVizColumn}
127
- label='Multi-Visualization Column'
128
- initial='--Select--'
129
- fieldName=''
130
- updateField={(section, subsection, fieldName, value) => setMultiVizColumn(value)}
131
- required
132
- />
129
+ <>
130
+ <InputSelect options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})} value={config.rows[rowIndex].multiVizColumn} label='Multi-Visualization Column' initial='--Select--' updateField={(section, subsection, fieldName, value) => setMultiVizColumn(value)} required />
131
+ <CheckBox value={config.rows[rowIndex].expandCollapseAllButtons} label=' Add Expand/Collapse All buttons' fieldName='' updateField={(section, subsection, fieldName, value) => setExpandCollapseAllButtons(value)} />
132
+ </>
133
133
  )
134
134
  ) : (
135
135
  <></>
@@ -0,0 +1,20 @@
1
+ type ExpandCollapseButtonsProps = {
2
+ setAllExpanded: Function
3
+ }
4
+
5
+ const ExpandCollapseButtons: React.FC<ExpandCollapseButtonsProps> = ({ setAllExpanded }) => {
6
+ return (
7
+ <div className='d-block '>
8
+ <div className='d-flex flex-row-reverse mb-2'>
9
+ <button className='btn expand-collapse-buttons' onClick={() => setAllExpanded(false)}>
10
+ - Collapse All
11
+ </button>
12
+ <button className='btn expand-collapse-buttons mr-2' onClick={() => setAllExpanded(true)}>
13
+ + Expand All
14
+ </button>
15
+ </div>
16
+ </div>
17
+ )
18
+ }
19
+
20
+ export default ExpandCollapseButtons