@cdc/dashboard 4.24.12 → 4.25.2-25

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 (60) hide show
  1. package/dist/cdcdashboard.js +74365 -72646
  2. package/examples/all-components.json +529 -4607
  3. package/examples/dashboard-gallery.json +397 -397
  4. package/examples/private/DEV-10120.json +1294 -0
  5. package/examples/private/DEV-10527.json +564 -0
  6. package/examples/private/DEV-10586.json +54319 -0
  7. package/examples/private/DEV-10856.json +54319 -0
  8. package/examples/private/DEV-9989.json +229 -0
  9. package/examples/private/art-dashboard.json +2 -2
  10. package/examples/private/bird-flu-2.json +440 -0
  11. package/examples/private/bird-flu.json +413 -0
  12. package/examples/private/dashboard-config-ehdi.json +29915 -0
  13. package/examples/private/dashboard-map-filter.json +815 -0
  14. package/examples/private/dashboard-margins.js +15 -0
  15. package/examples/private/dataset.json +1452 -0
  16. package/examples/private/dev-10856-2.json +1348 -0
  17. package/examples/private/ehdi-data.json +29502 -0
  18. package/examples/private/exposure-source-h5-data.csv +26 -0
  19. package/examples/private/feelings.json +1 -0
  20. package/examples/private/nhis.json +1792 -0
  21. package/examples/private/workforce.json +2041 -0
  22. package/index.html +5 -8
  23. package/package.json +9 -9
  24. package/src/CdcDashboard.tsx +5 -8
  25. package/src/CdcDashboardComponent.tsx +70 -60
  26. package/src/_stories/Dashboard.stories.tsx +63 -0
  27. package/src/_stories/_mock/dashboard-filter-asc.json +551 -0
  28. package/src/_stories/_mock/data-bite-dash-test.json +1 -0
  29. package/src/_stories/_mock/data-bite-dash-test_1.json +1 -0
  30. package/src/_stories/_mock/data-bite-dash-test_1_1.json +1 -0
  31. package/src/_stories/_mock/data-bite-dash-test_1_1_1.json +1 -0
  32. package/src/components/CollapsibleVisualizationRow.tsx +3 -3
  33. package/src/components/Column.tsx +12 -1
  34. package/src/components/DashboardFilters/DashboardFilters.tsx +14 -9
  35. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +23 -8
  36. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +13 -3
  37. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +130 -41
  38. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +10 -7
  39. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +11 -12
  40. package/src/components/DashboardFilters/dashboardfilter.styles.css +2 -2
  41. package/src/components/ExpandCollapseButtons.tsx +1 -1
  42. package/src/components/Header/Header.tsx +1 -2
  43. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +2 -2
  44. package/src/components/MultiConfigTabs/MultiTabs.tsx +1 -1
  45. package/src/components/VisualizationRow.tsx +13 -3
  46. package/src/components/Widget.tsx +9 -3
  47. package/src/helpers/addValuesToDashboardFilters.ts +6 -5
  48. package/src/helpers/apiFilterHelpers.ts +11 -6
  49. package/src/helpers/changeFilterActive.ts +17 -4
  50. package/src/helpers/getFilteredData.ts +13 -4
  51. package/src/helpers/getUpdateConfig.ts +11 -4
  52. package/src/helpers/loadAPIFilters.ts +6 -4
  53. package/src/helpers/tests/updatesChildFilters.test.ts +56 -0
  54. package/src/helpers/updateChildFilters.ts +50 -0
  55. package/src/index.tsx +1 -0
  56. package/src/scss/main.scss +1 -15
  57. package/src/store/dashboard.actions.ts +2 -2
  58. package/src/store/dashboard.reducer.ts +60 -29
  59. package/src/types/DashboardConfig.ts +2 -0
  60. package/src/types/SharedFilter.ts +1 -1
@@ -33,12 +33,13 @@ const labelHash = {
33
33
 
34
34
  type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number }
35
35
  type WidgetProps = {
36
+ title: string
36
37
  widgetConfig?: WidgetConfig
37
38
  addVisualization?: Function
38
39
  type: string
39
40
  }
40
41
 
41
- const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
42
+ const Widget = ({ title, widgetConfig, addVisualization, type }: WidgetProps) => {
42
43
  const { overlay } = useGlobalContext()
43
44
  const { config, data } = useContext(DashboardContext)
44
45
  const dispatch = useContext(DashboardDispatchContext)
@@ -74,9 +75,10 @@ const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
74
75
 
75
76
  const deleteWidget = () => {
76
77
  if (!widgetConfig) return
78
+
77
79
  dispatch({
78
80
  type: 'DELETE_WIDGET',
79
- payload: { rowIdx: widgetConfig.rowIdx, colIdx: widgetConfig.colIdx, uid: widgetConfig.uid }
81
+ payload: { uid: widgetConfig.uid as string }
80
82
  })
81
83
  }
82
84
 
@@ -102,7 +104,10 @@ const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
102
104
 
103
105
  const editWidget = () => {
104
106
  if (!widgetConfig) return
105
- dispatch({ type: 'UPDATE_VISUALIZATION', payload: { vizKey: widgetConfig.uid, configureData: { editing: true } } })
107
+ dispatch({
108
+ type: 'UPDATE_VISUALIZATION',
109
+ payload: { vizKey: widgetConfig.uid as string, configureData: { editing: true } }
110
+ })
106
111
  loadSampleData()
107
112
  }
108
113
 
@@ -156,6 +161,7 @@ const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
156
161
  )}
157
162
  {iconHash[type]}
158
163
  <span>{labelHash[type]}</span>
164
+ <span>{title}</span>
159
165
  {widgetConfig?.newViz && type !== 'dashboardFilters' && (
160
166
  <span onClick={editWidget} className='config-needed'>
161
167
  Configuration needed
@@ -1,6 +1,7 @@
1
1
  import _ from 'lodash'
2
2
  import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
3
3
  import { SharedFilter } from '../types/SharedFilter'
4
+ import { handleSorting } from '@cdc/core/components/Filters'
4
5
 
5
6
  // Gets filter values from dataset
6
7
  const generateValuesForFilter = (columnName: string, data: Record<string, any[]>) => {
@@ -38,8 +39,8 @@ export const addValuesToDashboardFilters = (
38
39
  if (filter.type === 'urlfilter') return filter
39
40
  const filterCopy = _.cloneDeep(filter)
40
41
  const filterValues = generateValuesForFilter(getSelector(filter), data)
41
-
42
42
  filterCopy.values = filterValues
43
+
43
44
  if (filterValues.length > 0) {
44
45
  const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
45
46
  if (queryStringFilterValue) {
@@ -49,11 +50,11 @@ export const addValuesToDashboardFilters = (
49
50
  const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
50
51
  filterCopy.active = active.filter(val => defaultValues.includes(val))
51
52
  } else {
52
- const defaultLabel = filters.find(filter => filter.resetLabel)
53
- const defaultValue = defaultLabel ? defaultLabel.resetLabel : filterCopy.values[0] || filterCopy.active
54
- filterCopy.active = defaultValue
53
+ const hasResetLabel = filters.find(filter => filter.resetLabel)
54
+ const defaultValue = hasResetLabel ? hasResetLabel.resetLabel : filterCopy.active || filterCopy.values[0]
55
+ filterCopy.active = filterCopy.defaultValue || defaultValue
55
56
  }
56
57
  }
57
- return filterCopy
58
+ return handleSorting(filterCopy)
58
59
  })
59
60
  }
@@ -134,10 +134,16 @@ export const setAutoLoadDefaultValue = (
134
134
  ): SharedFilter => {
135
135
  const sharedFiltersCopy = _.cloneDeep(sharedFilters)
136
136
  const sharedFilter = _.cloneDeep(sharedFiltersCopy[sharedFilterIndex])
137
- if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) return sharedFilter // no autoLoading happening
138
- const hasQueryParameter = sharedFilter.setByQueryParameter
139
- ? Boolean(getQueryParam(sharedFilter.setByQueryParameter))
140
- : false
137
+ const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
138
+ const hasQueryParameter = sharedFilter.setByQueryParameter ? defaultQueryParamValue !== undefined : false
139
+ if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) {
140
+ if (hasQueryParameter && sharedFilter.apiFilter) {
141
+ const subQueryValue = getQueryParam(sharedFilter.subGrouping?.setByQueryParameter)
142
+ const isNestedDropdown = subQueryValue !== undefined
143
+ sharedFilter.queuedActive = isNestedDropdown ? [defaultQueryParamValue, subQueryValue] : defaultQueryParamValue
144
+ }
145
+ return sharedFilter // no autoLoading happening
146
+ }
141
147
  if (autoLoadFilterIndexes.includes(sharedFilterIndex) || hasQueryParameter) {
142
148
  const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
143
149
  const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
@@ -148,11 +154,10 @@ export const setAutoLoadDefaultValue = (
148
154
  setActiveNestedDropdown(dropdownOptions, sharedFilter)
149
155
  } else {
150
156
  const defaultValue = dropdownOptions[0]?.value
151
- const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
152
157
  if (!sharedFilter.active) {
153
158
  sharedFilter.active = defaultQueryParamValue || defaultValue
154
159
  } else {
155
- const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
160
+ const currentOption = dropdownOptions.find(option => option.value == sharedFilter.active) // loose equality required: 2017 should equal '2017'
156
161
  sharedFilter.active = currentOption ? currentOption.value : defaultValue
157
162
  }
158
163
  }
@@ -1,6 +1,11 @@
1
1
  import _ from 'lodash'
2
2
  import { FilterBehavior } from '../helpers/FilterBehavior'
3
- import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
3
+ import {
4
+ getQueryParams,
5
+ removeQueryParam,
6
+ updateQueryParam,
7
+ updateQueryString
8
+ } from '@cdc/core/helpers/queryStringUtils'
4
9
  import { SharedFilter } from '../types/SharedFilter'
5
10
  import { DashboardFilters } from '../types/DashboardFilters'
6
11
  import { FILTER_STYLE } from '../types/FilterStyles'
@@ -12,9 +17,11 @@ const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
12
17
  .filter(i => i !== null)
13
18
  if (childFilterIndexes.length) {
14
19
  childFilterIndexes.forEach(filterIndex => {
15
- sharedFilters[filterIndex].active = ''
16
- if (sharedFilters[filterIndex].subGrouping) {
17
- sharedFilters[filterIndex].subGrouping.active = ''
20
+ const cur = sharedFilters[filterIndex]
21
+ if (cur.setByQueryParameter) removeQueryParam(cur.setByQueryParameter)
22
+ cur.active = ''
23
+ if (cur.subGrouping) {
24
+ cur.subGrouping.active = ''
18
25
  }
19
26
  })
20
27
  }
@@ -45,7 +52,13 @@ export const changeFilterActive = (
45
52
  updateQueryString(queryParams)
46
53
  }
47
54
  }
55
+ } else if (currentFilter.subGrouping) {
56
+ updateQueryParam(currentFilter.setByQueryParameter, value[0])
57
+ updateQueryParam(currentFilter.subGrouping.setByQueryParameter, value[1])
58
+ sharedFiltersCopy[filterIndex].queuedActive = value
48
59
  } else {
60
+ const paramVal = Array.isArray(value) ? value.join(',') : value
61
+ if (currentFilter.setByQueryParameter) updateQueryParam(currentFilter.setByQueryParameter, paramVal)
49
62
  sharedFiltersCopy[filterIndex].queuedActive = value
50
63
  }
51
64
  return [sharedFiltersCopy, handleChildren(sharedFiltersCopy, filterIndex)]
@@ -6,11 +6,18 @@ import { getFormattedData } from './getFormattedData'
6
6
  import { getVizKeys } from './getVizKeys'
7
7
 
8
8
  export const getApplicableFilters = (dashboard: Dashboard, key: string | number): false | SharedFilter[] => {
9
- const c = dashboard.sharedFilters?.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(`${key}`) !== -1)
9
+ const c = dashboard.sharedFilters?.filter(
10
+ sharedFilter =>
11
+ (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(`${key}`) !== -1) || sharedFilter.usedBy?.indexOf(key) !== -1
12
+ )
10
13
  return c?.length > 0 ? c : false
11
14
  }
12
15
 
13
- export const getFilteredData = (state: DashboardState, initialFilteredData?: Record<string, any>, dataOverride?: Object) => {
16
+ export const getFilteredData = (
17
+ state: DashboardState,
18
+ initialFilteredData?: Record<string, any>,
19
+ dataOverride?: Object
20
+ ) => {
14
21
  const newFilteredData = initialFilteredData || {}
15
22
  const { config } = state
16
23
  getVizKeys(config).forEach(key => {
@@ -18,7 +25,8 @@ export const getFilteredData = (state: DashboardState, initialFilteredData?: Rec
18
25
  if (applicableFilters) {
19
26
  const { dataKey, data, dataDescription } = config.visualizations[key]
20
27
  const _data = (dataOverride || state.data)[dataKey] || data
21
- const formattedData = dataOverride?.[dataKey] || (dataDescription ? getFormattedData(_data, dataDescription) : _data)
28
+ const formattedData =
29
+ dataOverride?.[dataKey] || (dataDescription ? getFormattedData(_data, dataDescription) : _data)
22
30
 
23
31
  newFilteredData[key] = filterData(applicableFilters, formattedData)
24
32
  }
@@ -29,7 +37,8 @@ export const getFilteredData = (state: DashboardState, initialFilteredData?: Rec
29
37
  const { dataKey, data, dataDescription } = row
30
38
  const _data = (dataOverride || state.data)[dataKey] || data
31
39
  if (applicableFilters) {
32
- const formattedData = dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
40
+ const formattedData =
41
+ dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
33
42
 
34
43
  newFilteredData[index] = filterData(applicableFilters, formattedData)
35
44
  } else {
@@ -31,7 +31,7 @@ export const getUpdateConfig =
31
31
  const defaultValues = _filter.pivot ? _filter.values : _filter.values[0]
32
32
 
33
33
  const queryStringFilterValue = getQueryStringFilterValue(_filter)
34
- if(queryStringFilterValue){
34
+ if (queryStringFilterValue) {
35
35
  _filter.active = queryStringFilterValue
36
36
  } else {
37
37
  _filter.active = _filter.active || defaultValues
@@ -57,19 +57,26 @@ export const getUpdateConfig =
57
57
  visualizationKeys.forEach(visualizationKey => {
58
58
  const row = vizRowColumnLocator[visualizationKey]
59
59
  if (newConfig.rows[row]?.datakey) return // data configured on the row level
60
- const applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
60
+ const applicableFilters = newConfig.dashboard.sharedFilters.filter(
61
+ sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1
62
+ )
61
63
 
62
64
  if (applicableFilters.length > 0) {
63
65
  const visualization = newConfig.visualizations[visualizationKey]
64
66
  const _newConfigDataSet = newConfig.datasets[visualization.dataKey]
65
- const formattedData = getFormattedData(_newConfigDataSet?.data || visualization.data, visualization.dataDescription)
67
+ const formattedData = getFormattedData(
68
+ _newConfigDataSet?.data || visualization.data,
69
+ visualization.dataDescription
70
+ )
66
71
  const _data = formattedData || (dataOverride || state.data)[visualization.dataKey]
67
72
  newFilteredData[visualizationKey] = filterData(applicableFilters, _data)
68
73
  }
69
74
  })
70
75
 
71
76
  newConfig.rows.forEach((row, rowIndex) => {
72
- const applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(rowIndex) !== -1)
77
+ const applicableFilters = newConfig.dashboard.sharedFilters.filter(
78
+ sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(rowIndex) !== -1
79
+ )
73
80
 
74
81
  if (applicableFilters.length > 0) {
75
82
  const formattedData = getFormattedData(row.data, row.dataDescription)
@@ -42,7 +42,7 @@ export const loadAPIFiltersFactory = (
42
42
  return Promise.all(
43
43
  Object.keys(toFetch).map(
44
44
  endpoint =>
45
- new Promise<void>(resolve => {
45
+ new Promise<{ error: boolean }>(resolve => {
46
46
  fetch(endpoint)
47
47
  .then(resp => resp.json())
48
48
  .then(data => {
@@ -67,13 +67,15 @@ export const loadAPIFiltersFactory = (
67
67
  type: 'ADD_ERROR_MESSAGE',
68
68
  payload: 'There was a problem returning data. Please try again.'
69
69
  })
70
+ resolve({ error: true })
70
71
  })
71
72
  .finally(() => {
72
- resolve()
73
+ resolve({ error: false })
73
74
  })
74
75
  })
75
76
  )
76
- ).then(() => {
77
+ ).then(responses => {
78
+ const hasError = responses.some(({ error }) => error)
77
79
  const toLoad = sharedFilters.reduce((acc, curr, index) => {
78
80
  // the filter is autoloading and it hasn't finished yet
79
81
  if (_autoLoadFilterIndexes.includes(index) && !curr.active) {
@@ -84,7 +86,7 @@ export const loadAPIFiltersFactory = (
84
86
  }
85
87
  return acc
86
88
  }, [])
87
- if (!toLoad.length || recursiveLimit === 0) {
89
+ if (hasError || !toLoad.length || recursiveLimit === 0) {
88
90
  setAPIFilterDropdowns(newDropdowns)
89
91
  dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
90
92
  return sharedFilters
@@ -0,0 +1,56 @@
1
+ import { SharedFilter } from '../../types/SharedFilter'
2
+ import { updateChildFilters } from '../updateChildFilters'
3
+
4
+ describe('updateChildFilters', () => {
5
+ it('should filter data based on the provided filters', () => {
6
+ const filters = [
7
+ {
8
+ tier: 1,
9
+ columnName: 'name',
10
+ active: 'John',
11
+ key: 'Parent Filter',
12
+ values: ['John', 'Kelly', 'Norman', 'Jane']
13
+ },
14
+ {
15
+ tier: 2,
16
+ columnName: 'lastName',
17
+ active: '',
18
+ key: 'Child Filter',
19
+ parents: 'Parent Filter',
20
+ values: ['Deer', 'Roberts']
21
+ }
22
+ ] as SharedFilter[]
23
+ const data = {
24
+ vizKey: [
25
+ [
26
+ { name: 'John', lastName: 'Deer' },
27
+ { name: 'John', lastName: 'Roberts' },
28
+ { name: 'Kelly', lastName: 'Adams' },
29
+ { name: 'Norman', lastName: 'Sally' },
30
+ { name: 'Jane', lastName: 'Gorman' }
31
+ ]
32
+ ]
33
+ }
34
+
35
+ let exprectedResult = [
36
+ {
37
+ tier: 1,
38
+ columnName: 'name',
39
+ active: 'John',
40
+ key: 'Parent Filter',
41
+ values: ['John', 'Kelly', 'Norman', 'Jane']
42
+ },
43
+ {
44
+ tier: 2,
45
+ columnName: 'lastName',
46
+ active: '',
47
+ key: 'Child Filter',
48
+ parents: 'Parent Filter',
49
+ values: ['Deer', 'Roberts'] // updated values only
50
+ }
51
+ ]
52
+ const result = updateChildFilters(filters, data)
53
+
54
+ expect(result).toEqual(exprectedResult)
55
+ })
56
+ })
@@ -0,0 +1,50 @@
1
+ import { SharedFilter } from '../types/SharedFilter'
2
+ import _ from 'lodash'
3
+
4
+ export const updateChildFilters = (newSharedFilters: SharedFilter[], data: Record<string, any>): SharedFilter[] => {
5
+ const dataSet = Object.values(data).flat()
6
+
7
+ // Find indexes of all child filters
8
+ const childFilterIndexes: number[] = newSharedFilters
9
+ .map((filter, index) => (filter.type === 'datafilter' && filter.parents ? index : -1))
10
+ .filter(index => index !== -1)
11
+ if (childFilterIndexes.length === 0) return newSharedFilters
12
+
13
+ // deep copy of the shared filters
14
+ const updatedFilters = _.cloneDeep(newSharedFilters)
15
+
16
+ // Update each child filter
17
+ childFilterIndexes.forEach(childIndex => {
18
+ const childFilter: SharedFilter = newSharedFilters[childIndex]
19
+ const parentFilter: SharedFilter = newSharedFilters.find(
20
+ filter => String(childFilter.parents) === String(filter.key)
21
+ )
22
+
23
+ if (parentFilter) {
24
+ // Filter dataset based on parent's active value
25
+ const parentsActiveValues: string[] = dataSet.filter((d: Record<string, any>) => {
26
+ return parentFilter.active?.includes(d[parentFilter.columnName])
27
+ })
28
+ // Get unique active values for the child filter
29
+ const childFilterValues = _.uniq(parentsActiveValues.map(d => d[childFilter.columnName]).filter(Boolean))
30
+
31
+ // Update the child filter if unique values exist
32
+ if (childFilterValues.length > 0) {
33
+ const isChildMultiSelect = childFilter.filterStyle === 'multi-select'
34
+ const activeValue = isChildMultiSelect
35
+ ? childFilterValues
36
+ : childFilter.active
37
+ ? childFilter.active
38
+ : childFilter.defaultValue
39
+ ? childFilter.defaultValue
40
+ : childFilterValues[0]
41
+ updatedFilters[childIndex] = {
42
+ ...childFilter,
43
+ values: childFilterValues,
44
+ active: activeValue
45
+ }
46
+ }
47
+ }
48
+ })
49
+ return updatedFilters
50
+ }
package/src/index.tsx CHANGED
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  import ReactDOM from 'react-dom/client'
3
3
 
4
4
  import MultiDashboardWrapper from './CdcDashboard'
5
+ import '@cdc/core/styles/cove-main.scss'
5
6
  import './coreStyles_dashboard.scss'
6
7
 
7
8
  let isEditor = window.location.href.includes('editor=true')
@@ -138,7 +138,6 @@
138
138
  }
139
139
 
140
140
  .btn {
141
-
142
141
  // Expand and Collapse Buttons for Multiviz Dashboard
143
142
  &.expand-collapse-buttons {
144
143
  background-color: var(--lightestGray);
@@ -163,13 +162,6 @@
163
162
  margin: 15px 0 0;
164
163
  }
165
164
 
166
- .data-table-container {
167
- margin: 20px 0 0;
168
- &.download-link-above {
169
- margin: 0;
170
- }
171
- }
172
-
173
165
  .collapsable-multiviz-container {
174
166
  position: relative;
175
167
  border: var(--lightGray) 1px solid;
@@ -193,16 +185,12 @@
193
185
  position: relative;
194
186
  }
195
187
  @include breakpoint(xs) {
196
- $font-small: 0.7em;
197
- font-size: $font-small + 0.2em;
188
+ font-size: 0.9em;
198
189
  }
199
190
  }
200
191
  .data-table-heading {
201
192
  display: none;
202
193
  }
203
- .table-container {
204
- margin: 0 1em;
205
- }
206
194
  }
207
195
 
208
196
  .dashboard-download-link {
@@ -283,8 +271,6 @@
283
271
  }
284
272
 
285
273
  .cdc-dashboard-inner-container {
286
- margin: 1em;
287
-
288
274
  &.is-editor {
289
275
  margin-top: 7em;
290
276
  }
@@ -9,12 +9,12 @@ import { SharedFilter } from '../types/SharedFilter'
9
9
  type ADD_FOOTNOTE = Action<'ADD_FOOTNOTE', { id: string; rowIndex: number; config: Footnotes }>
10
10
  type ADD_VISUALIZATION = Action<'ADD_VISUALIZATION', { rowIdx: number; colIdx: number; newViz: AnyVisualization }>
11
11
  type APPLY_CONFIG = Action<'APPLY_CONFIG', [Config, Object?]>
12
- type DELETE_WIDGET = Action<'DELETE_WIDGET', { rowIdx: number; colIdx: number; uid: string }>
12
+ type DELETE_WIDGET = Action<'DELETE_WIDGET', { uid: string }>
13
13
  type MOVE_VISUALIZATION = Action<
14
14
  'MOVE_VISUALIZATION',
15
15
  { rowIdx: number; colIdx: number; widget: AnyVisualization & { rowIdx: number; colIdx: number } }
16
16
  >
17
- type SET_CONFIG = Action<'SET_CONFIG', Partial<Config>>
17
+ type SET_CONFIG = Action<'SET_CONFIG', Partial<Config> & { activeDashboard?: number }>
18
18
  type UPDATE_CONFIG = Action<'UPDATE_CONFIG', [Config, Object?]>
19
19
  type SET_DATA = Action<'SET_DATA', Record<string, any[]>>
20
20
  type SET_LOADING = Action<'SET_LOADING', boolean>
@@ -1,23 +1,23 @@
1
1
  import _ from 'lodash'
2
2
  import { getUpdateConfig } from '../helpers/getUpdateConfig'
3
- import { MultiDashboardConfig } from '../types/MultiDashboard'
3
+ import { MultiDashboard, MultiDashboardConfig } from '../types/MultiDashboard'
4
4
  import DashboardActions from './dashboard.actions'
5
5
  import { devToolsWrapper } from '@cdc/core/helpers/withDevTools'
6
6
  import { Tab } from '../types/Tab'
7
- import { DashboardConfig } from '../types/DashboardConfig'
7
+ import { Dashboard } from '../types/Dashboard'
8
8
  import { ConfigRow } from '../types/ConfigRow'
9
9
  import { AnyVisualization } from '@cdc/core/types/Visualization'
10
10
  import { initialState } from '../DashboardContext'
11
11
 
12
12
  type BlankMultiConfig = {
13
- dashboard: Partial<DashboardConfig>
13
+ dashboard: Partial<Dashboard>
14
14
  rows: Partial<ConfigRow>[]
15
15
  visualizations: Record<string, Object>
16
16
  table: Object
17
17
  }
18
18
 
19
19
  const createBlankDashboard: () => BlankMultiConfig = () => ({
20
- dashboard: {},
20
+ dashboard: { sharedFilters: [] },
21
21
  rows: [{ columns: [{ width: 12 }] }],
22
22
  visualizations: {},
23
23
  table: {
@@ -44,7 +44,10 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
44
44
  const newRows = state.config.rows.map((row, i) => (i === rowIndex ? { ...row, footnotesId: id } : row))
45
45
  return {
46
46
  ...state,
47
- config: { ...state.config, rows: newRows, visualizations: { ...state.config.visualizations, [id]: config } }
47
+ config: saveMultiChanges(
48
+ { ...state.config, rows: newRows, visualizations: { ...state.config.visualizations, [id]: config } },
49
+ state.config.activeDashboard
50
+ )
48
51
  }
49
52
  }
50
53
  case 'ADD_NEW_DASHBOARD': {
@@ -55,7 +58,7 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
55
58
  }
56
59
  case 'UPDATE_CONFIG': {
57
60
  const [config, filteredData] = getUpdateConfig(state)(...action.payload)
58
- return { ...state, config, filteredData }
61
+ return { ...state, config: saveMultiChanges(config, state.config.activeDashboard), filteredData }
59
62
  }
60
63
  case 'APPLY_CONFIG': {
61
64
  // using advanced editor. Wipe all existing data and apply new config
@@ -68,10 +71,18 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
68
71
  if (data) acc[key] = data
69
72
  return acc
70
73
  }, {})
71
- return { ...initialState, config, filteredData, data }
74
+ return { ...initialState, config: saveMultiChanges(config, state.config.activeDashboard), filteredData, data }
72
75
  }
73
76
  case 'SET_CONFIG': {
74
- return { ...state, config: { ...state.config, ...action.payload } }
77
+ if (
78
+ action.payload.activeDashboard === undefined ||
79
+ state.config.activeDashboard === action.payload.activeDashboard
80
+ ) {
81
+ return {
82
+ ...state,
83
+ config: saveMultiChanges({ ...state.config, ...action.payload }, action.payload.activeDashboard)
84
+ }
85
+ } else return state // ignore SET_CONFIG calls that have the wrong activeDashboard due to async api fetching
75
86
  }
76
87
  case 'SET_DATA': {
77
88
  return { ...state, data: action.payload }
@@ -88,14 +99,10 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
88
99
  case 'SET_SHARED_FILTERS': {
89
100
  const newSharedFilters = action.payload
90
101
  const newDashboardConfig = { ...state.config.dashboard, sharedFilters: newSharedFilters }
91
- if (state.config.multiDashboards) {
92
- const saveSlot = state.config.activeDashboard
93
- const newMultiDashboards = _.cloneDeep(state.config.multiDashboards)
94
- newMultiDashboards[saveSlot].dashboard = newDashboardConfig
95
- const newState = applyMultiDashboards(state, newMultiDashboards)
96
- return { ...newState, config: { ...newState.config, dashboard: newDashboardConfig } }
102
+ return {
103
+ ...state,
104
+ config: saveMultiChanges({ ...state.config, dashboard: newDashboardConfig }, state.config.activeDashboard)
97
105
  }
98
- return { ...state, config: { ...state.config, dashboard: newDashboardConfig } }
99
106
  }
100
107
  case 'SET_TAB_SELECTED': {
101
108
  return { ...state, tabSelected: action.payload }
@@ -138,7 +145,8 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
138
145
  const label = newMultiDashboards[saveSlot].label
139
146
  const toSave = _.pick(state.config, ['dashboard', 'visualizations', 'rows'])
140
147
  newMultiDashboards[saveSlot] = { ...toSave, label }
141
- return applyMultiDashboards(state, newMultiDashboards)
148
+ const newConfig = saveMultiChanges(state.config, saveSlot)
149
+ return { ...state, config: newConfig }
142
150
  }
143
151
  case 'INITIALIZE_MULTIDASHBOARDS': {
144
152
  const label = 'New Dashboard 1'
@@ -171,7 +179,10 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
171
179
  newRows[rowIdx].columns[colIdx].widget = vizKey
172
180
  return {
173
181
  ...state,
174
- config: { ...state.config, visualizations: { ...state.config.visualizations, [vizKey]: newViz }, rows: newRows }
182
+ config: saveMultiChanges(
183
+ { ...state.config, visualizations: { ...state.config.visualizations, [vizKey]: newViz }, rows: newRows },
184
+ state.config.activeDashboard
185
+ )
175
186
  }
176
187
  }
177
188
  case 'MOVE_VISUALIZATION': {
@@ -181,7 +192,7 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
181
192
  newRows[rowIdx].columns[colIdx].widget = widget.uid
182
193
  return {
183
194
  ...state,
184
- config: { ...state.config, rows: newRows }
195
+ config: saveMultiChanges({ ...state.config, rows: newRows }, state.config.activeDashboard)
185
196
  }
186
197
  }
187
198
  case 'UPDATE_VISUALIZATION': {
@@ -189,7 +200,10 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
189
200
  const updatedViz = { ...state.config.visualizations[vizKey], ...configureData } as AnyVisualization
190
201
  return {
191
202
  ...state,
192
- config: { ...state.config, visualizations: { ...state.config.visualizations, [vizKey]: updatedViz } }
203
+ config: saveMultiChanges(
204
+ { ...state.config, visualizations: { ...state.config.visualizations, [vizKey]: updatedViz } },
205
+ state.config.activeDashboard
206
+ )
193
207
  }
194
208
  }
195
209
  case 'UPDATE_ROW': {
@@ -200,12 +214,11 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
200
214
  }
201
215
  return row
202
216
  })
203
- return { ...state, config: { ...state.config, rows: newRows } }
217
+ return { ...state, config: saveMultiChanges({ ...state.config, rows: newRows }, state.config.activeDashboard) }
204
218
  }
205
219
  case 'DELETE_WIDGET': {
206
- const { rowIdx, colIdx, uid } = action.payload
220
+ const { uid } = action.payload
207
221
  const newRows = _.cloneDeep(state.config.rows)
208
- newRows[rowIdx].columns[colIdx].widget = null
209
222
  const newVisualizations = _.cloneDeep(state.config.visualizations)
210
223
  delete newVisualizations[uid]
211
224
  const newSharedFilters = _.cloneDeep(state.config.dashboard.sharedFilters)
@@ -216,14 +229,23 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
216
229
  }
217
230
  })
218
231
  }
232
+
233
+ const filteredRows = _.map(newRows, row => ({
234
+ ...row,
235
+ columns: _.filter(row.columns, column => column.widget !== uid)
236
+ }))
237
+
219
238
  return {
220
239
  ...state,
221
- config: {
222
- ...state.config,
223
- dashboard: { ...state.config.dashboard, sharedFilters: newSharedFilters },
224
- visualizations: newVisualizations,
225
- rows: newRows
226
- }
240
+ config: saveMultiChanges(
241
+ {
242
+ ...state.config,
243
+ dashboard: { ...state.config.dashboard, sharedFilters: newSharedFilters },
244
+ visualizations: newVisualizations,
245
+ rows: filteredRows
246
+ },
247
+ state.config.activeDashboard
248
+ )
227
249
  }
228
250
  }
229
251
  default:
@@ -231,7 +253,16 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
231
253
  }
232
254
  }
233
255
 
234
- const applyMultiDashboards = (state, newMultiDashboards) => ({
256
+ const saveMultiChanges = (config: MultiDashboardConfig, saveSlot?: number): MultiDashboardConfig => {
257
+ if (saveSlot === undefined || !config.multiDashboards) return config
258
+ const newMultiDashboards = [...config.multiDashboards]
259
+ const label = newMultiDashboards[saveSlot].label
260
+ const toSave = _.pick(config, ['dashboard', 'visualizations', 'rows'])
261
+ newMultiDashboards[saveSlot] = { ...toSave, label }
262
+ return { ...config, multiDashboards: newMultiDashboards }
263
+ }
264
+
265
+ const applyMultiDashboards = (state: DashboardState, newMultiDashboards: MultiDashboard[]): DashboardState => ({
235
266
  ...state,
236
267
  config: { ...state.config, multiDashboards: newMultiDashboards }
237
268
  })
@@ -5,6 +5,7 @@ import { ConfigRow } from './ConfigRow'
5
5
  import { AnyVisualization } from '@cdc/core/types/Visualization'
6
6
  import { Table } from '@cdc/core/types/Table'
7
7
  import { Dashboard } from './Dashboard'
8
+ import { Version } from '@cdc/core/types/Version'
8
9
 
9
10
  export type DashboardConfig = DataSet & {
10
11
  dashboard: Dashboard
@@ -18,4 +19,5 @@ export type DashboardConfig = DataSet & {
18
19
  runtime: Runtime
19
20
  downloadImageButton: boolean
20
21
  downloadPdfButton: boolean
22
+ version: Version
21
23
  }