@cdc/dashboard 4.24.5 → 4.24.9

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 (87) hide show
  1. package/dist/cdcdashboard.js +144406 -127510
  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/examples/single-state-dashboard-filters.json +421 -0
  8. package/examples/state-level.json +90136 -0
  9. package/examples/state-points.json +10474 -0
  10. package/examples/test-file.json +147 -0
  11. package/examples/testing.json +94456 -0
  12. package/index.html +25 -4
  13. package/package.json +12 -11
  14. package/src/CdcDashboard.tsx +5 -1
  15. package/src/CdcDashboardComponent.tsx +250 -327
  16. package/src/DashboardContext.tsx +15 -1
  17. package/src/_stories/Dashboard.stories.tsx +158 -40
  18. package/src/_stories/_mock/api-filter-chart.json +11 -35
  19. package/src/_stories/_mock/api-filter-map.json +17 -31
  20. package/src/_stories/_mock/bump-chart.json +3554 -0
  21. package/src/_stories/_mock/methodology.json +412 -0
  22. package/src/_stories/_mock/methodologyAPI.ts +90 -0
  23. package/src/_stories/_mock/multi-viz.json +3 -4
  24. package/src/_stories/_mock/pivot-filter.json +14 -12
  25. package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
  26. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  27. package/src/components/Column.tsx +1 -1
  28. package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
  29. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  30. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  31. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
  32. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  33. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
  34. package/src/components/DashboardFilters/index.ts +3 -0
  35. package/src/components/DataDesignerModal.tsx +9 -9
  36. package/src/components/ExpandCollapseButtons.tsx +20 -0
  37. package/src/components/Header/Header.tsx +1 -102
  38. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
  39. package/src/components/Row.tsx +52 -19
  40. package/src/components/Toggle/Toggle.tsx +2 -4
  41. package/src/components/VisualizationRow.tsx +169 -30
  42. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  43. package/src/components/VisualizationsPanel/index.ts +1 -0
  44. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  45. package/src/components/Widget.tsx +27 -90
  46. package/src/helpers/FilterBehavior.ts +4 -0
  47. package/src/helpers/addValuesToDashboardFilters.ts +49 -0
  48. package/src/helpers/apiFilterHelpers.ts +102 -0
  49. package/src/helpers/changeFilterActive.ts +39 -0
  50. package/src/helpers/filterData.ts +10 -48
  51. package/src/helpers/generateValuesForFilter.ts +1 -1
  52. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  53. package/src/helpers/getFilteredData.ts +7 -5
  54. package/src/helpers/getVizConfig.ts +23 -2
  55. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  56. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  57. package/src/helpers/iconHash.tsx +5 -3
  58. package/src/helpers/loadAPIFilters.ts +74 -0
  59. package/src/helpers/mapDataToConfig.ts +29 -0
  60. package/src/helpers/processData.ts +2 -3
  61. package/src/helpers/reloadURLHelpers.ts +78 -0
  62. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
  63. package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
  64. package/src/helpers/tests/filterData.test.ts +1 -93
  65. package/src/helpers/tests/getFilteredData.test.ts +86 -0
  66. package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +176 -0
  67. package/src/helpers/tests/reloadURLHelpers.test.ts +195 -0
  68. package/src/scss/editor-panel.scss +1 -1
  69. package/src/scss/grid.scss +34 -27
  70. package/src/scss/main.scss +41 -3
  71. package/src/scss/variables.scss +4 -0
  72. package/src/store/dashboard.actions.ts +12 -4
  73. package/src/store/dashboard.reducer.ts +30 -4
  74. package/src/types/APIFilter.ts +1 -5
  75. package/src/types/ConfigRow.ts +2 -0
  76. package/src/types/Dashboard.ts +1 -1
  77. package/src/types/DashboardConfig.ts +2 -4
  78. package/src/types/DashboardFilters.ts +7 -0
  79. package/src/types/InitialState.ts +1 -1
  80. package/src/types/MultiDashboard.ts +2 -2
  81. package/src/types/SharedFilter.ts +4 -6
  82. package/src/types/Tab.ts +1 -1
  83. package/LICENSE +0 -201
  84. package/src/components/Filters.tsx +0 -88
  85. package/src/components/Header/FilterModal.tsx +0 -510
  86. package/src/components/VisualizationsPanel.tsx +0 -95
  87. package/src/helpers/getApiFilterKey.ts +0 -5
@@ -0,0 +1,86 @@
1
+ import _ from 'lodash'
2
+ import { SharedFilter } from '../../types/SharedFilter'
3
+ import { getFilteredData } from '../getFilteredData'
4
+
5
+ describe('getFilteredData', () => {
6
+ const sharedFilterDefaults = { values: [], showDropdown: true, id: 123, parents: [], key: 'key' }
7
+ const data = {
8
+ data1: [
9
+ { id: 1, name: 'Alice', age: 25 },
10
+ { id: 2, name: 'Bob', age: 30 },
11
+ { id: 3, name: 'Charlie', age: 35 }
12
+ ]
13
+ }
14
+ const state = {
15
+ data,
16
+ config: {
17
+ dashboard: {
18
+ sharedFilters: []
19
+ },
20
+ visualizations: {
21
+ vizA: { dataKey: 'data1' }
22
+ },
23
+ rows: [{ dataKey: 'data1' }]
24
+ }
25
+ }
26
+
27
+ it('should apply data to rows when there are no applicable filters', () => {
28
+ expect(getFilteredData(state)).toEqual({ '0': data.data1 })
29
+ })
30
+
31
+ it('should filter visualizations', () => {
32
+ const sharedFilters: SharedFilter[] = [
33
+ {
34
+ usedBy: ['vizA'],
35
+ active: 'Alice',
36
+ columnName: 'name',
37
+ ...sharedFilterDefaults
38
+ }
39
+ ]
40
+ const config = { ...state.config, dashboard: { sharedFilters } }
41
+ expect(getFilteredData({ ...state, config })).toEqual({ '0': data.data1, vizA: [data.data1[0]] })
42
+ })
43
+
44
+ it('should filter visualizations and rows', () => {
45
+ const sharedFilters: SharedFilter[] = [
46
+ {
47
+ usedBy: ['vizA', '0'],
48
+ active: 'Alice',
49
+ columnName: 'name',
50
+ ...sharedFilterDefaults
51
+ }
52
+ ]
53
+ const config = { ...state.config, dashboard: { sharedFilters } }
54
+ expect(getFilteredData({ ...state, config })).toEqual({ '0': [data.data1[0]], vizA: [data.data1[0]] })
55
+ })
56
+
57
+ it('should use initialFilteredData', () => {
58
+ const initialFilteredData = { newData: [data.data1[1]] }
59
+ const sharedFilters: SharedFilter[] = [
60
+ {
61
+ usedBy: ['vizA', '0'],
62
+ active: 'Alice',
63
+ columnName: 'name',
64
+ ...sharedFilterDefaults
65
+ }
66
+ ]
67
+ const config = { ...state.config, dashboard: { sharedFilters } }
68
+ expect(getFilteredData({ ...state, config }, initialFilteredData)).toEqual({ newData: [data.data1[1]], '0': [data.data1[0]], vizA: [data.data1[0]] })
69
+ })
70
+
71
+ it('should filter visualizations and rows', () => {
72
+ const sharedFilters: SharedFilter[] = [
73
+ {
74
+ usedBy: ['vizA', '0'],
75
+ active: 'Alice',
76
+ columnName: 'name',
77
+ ...sharedFilterDefaults
78
+ }
79
+ ]
80
+ const dataOverride = _.cloneDeep(data)
81
+ dataOverride.data1[0] = { id: 1, name: 'Alice', age: 30 }
82
+ const config = { ...state.config, dashboard: { sharedFilters } }
83
+ const filteredData = getFilteredData({ ...state, config }, undefined, dataOverride)
84
+ expect(filteredData.vizA[0].age).toEqual(30)
85
+ })
86
+ })
@@ -0,0 +1,176 @@
1
+ import { vi } from 'vitest'
2
+ import { loadAPIFiltersFactory } from '../loadAPIFilters'
3
+ import { faker } from '@faker-js/faker'
4
+ import _ from 'lodash'
5
+ import { SharedFilter } from '../../types/SharedFilter'
6
+
7
+ faker.seed(123)
8
+
9
+ const endpointMockData = {
10
+ 'cdc.gov/filters/Sex': [
11
+ { Sex: 'male', Abbreviation: 'M' },
12
+ { Sex: 'female', Abbreviation: 'F' }
13
+ ],
14
+ 'cdc.gov/filters/Quarter': [{ Quarter: 'Q1' }, { Quarter: 'Q2' }, { Quarter: 'Q3' }, { Quarter: 'Q4' }],
15
+ 'cdc.gov/filters/YearQuarter': [
16
+ { Year: 2020, YearQuarter: '2020Q1' },
17
+ { Year: 2021, YearQuarter: '2021Q1' }
18
+ ]
19
+ }
20
+
21
+ const sharedFilters = [
22
+ {
23
+ key: 'Sex',
24
+ active: 'F',
25
+ showDropdown: true,
26
+ type: 'urlfilter',
27
+ apiFilter: {
28
+ apiEndpoint: 'cdc.gov/filters/Sex',
29
+ valueSelector: 'Abbreviation',
30
+ textSelector: 'Sex'
31
+ }
32
+ },
33
+ {
34
+ key: 'Quarter',
35
+ showDropdown: true,
36
+ type: 'urlfilter',
37
+ apiFilter: {
38
+ apiEndpoint: 'cdc.gov/filters/Quarter',
39
+ valueSelector: 'Quarter',
40
+ textSelector: ''
41
+ },
42
+ parents: ['Sex']
43
+ },
44
+ {
45
+ key: 'YearQuarter',
46
+ showDropdown: true,
47
+ type: 'urlfilter',
48
+ apiFilter: {
49
+ apiEndpoint: 'cdc.gov/filters/YearQuarter',
50
+ valueSelector: 'Year',
51
+ textSelector: 'YearQuarter'
52
+ },
53
+ parents: ['Sex', 'Quarter'],
54
+ multiSelect: true
55
+ }
56
+ ] as SharedFilter[]
57
+
58
+ const fetch = vi.fn(endpoint => {
59
+ const baseEndpoint = endpoint.split('?')[0]
60
+ return Promise.resolve({
61
+ json: () => Promise.resolve(endpointMockData[baseEndpoint])
62
+ })
63
+ })
64
+
65
+ global.fetch = fetch
66
+
67
+ describe('loadAPIFiltersFactory', () => {
68
+ const dispatch = vi.fn()
69
+ const setAPIFilterDropdowns = vi.fn()
70
+ const apiFilterDropdowns = {
71
+ 'cdc.gov/filters/Sex': [
72
+ { text: 'male', value: 'M' },
73
+ { text: 'female', value: 'F' }
74
+ ]
75
+ }
76
+ afterEach(() => {
77
+ vi.restoreAllMocks()
78
+ })
79
+ const loadAPIFilters = loadAPIFiltersFactory(dispatch, setAPIFilterDropdowns, [2])
80
+ it('creates a function', () => {
81
+ expect(typeof loadAPIFilters).toEqual('function')
82
+ })
83
+
84
+ it('loadAPIFilters() should return a promise', async () => {
85
+ const result = loadAPIFilters([], apiFilterDropdowns)
86
+ expect(result).toBeInstanceOf(Promise)
87
+ await result
88
+ })
89
+
90
+ it('loadAPIFilters() load dropdowns for children when parent is selected', async () => {
91
+ const newSharedFilters = await loadAPIFilters(sharedFilters, apiFilterDropdowns)
92
+ expect(dispatch).toHaveBeenCalledTimes(1)
93
+ expect(setAPIFilterDropdowns).toHaveBeenCalledTimes(1)
94
+
95
+ const expectedDropdowns = {
96
+ 'cdc.gov/filters/Quarter': [
97
+ {
98
+ text: 'Q1',
99
+ value: 'Q1'
100
+ },
101
+ {
102
+ text: 'Q2',
103
+ value: 'Q2'
104
+ },
105
+ {
106
+ text: 'Q3',
107
+ value: 'Q3'
108
+ },
109
+ {
110
+ text: 'Q4',
111
+ value: 'Q4'
112
+ }
113
+ ],
114
+ 'cdc.gov/filters/Sex': [
115
+ {
116
+ text: 'male',
117
+ value: 'M'
118
+ },
119
+ {
120
+ text: 'female',
121
+ value: 'F'
122
+ }
123
+ ]
124
+ }
125
+
126
+ expect(setAPIFilterDropdowns).toHaveBeenCalledWith(expectedDropdowns)
127
+ expect(fetch).toHaveBeenCalledTimes(1)
128
+ expect(newSharedFilters).toEqual(sharedFilters)
129
+ })
130
+
131
+ it('loadAPIFilters() load dropdowns for children when parents are selected, and autoload any autoloading filters', async () => {
132
+ sharedFilters[1].active = 'Q1'
133
+ const newSharedFilters = await loadAPIFilters(sharedFilters, apiFilterDropdowns)
134
+ expect(dispatch).toHaveBeenCalledTimes(1)
135
+ expect(setAPIFilterDropdowns).toHaveBeenCalledTimes(1)
136
+
137
+ const expectedDropdowns = {
138
+ 'cdc.gov/filters/Quarter': [
139
+ {
140
+ text: 'Q1',
141
+ value: 'Q1'
142
+ },
143
+ {
144
+ text: 'Q2',
145
+ value: 'Q2'
146
+ },
147
+ {
148
+ text: 'Q3',
149
+ value: 'Q3'
150
+ },
151
+ {
152
+ text: 'Q4',
153
+ value: 'Q4'
154
+ }
155
+ ],
156
+ 'cdc.gov/filters/Sex': [
157
+ {
158
+ text: 'male',
159
+ value: 'M'
160
+ },
161
+ {
162
+ text: 'female',
163
+ value: 'F'
164
+ }
165
+ ],
166
+ 'cdc.gov/filters/YearQuarter': [
167
+ { value: 2020, text: '2020Q1' },
168
+ { value: 2021, text: '2021Q1' }
169
+ ]
170
+ }
171
+
172
+ expect(setAPIFilterDropdowns).toHaveBeenCalledWith(expectedDropdowns)
173
+ expect(fetch).toHaveBeenCalledTimes(2)
174
+ expect(newSharedFilters[2].active).toEqual([2020])
175
+ })
176
+ })
@@ -0,0 +1,195 @@
1
+ import { isUpdateNeeded, getDataURL, getNewFileName, filterUsedByDataUrl } from '../reloadURLHelpers'
2
+ import { SharedFilter } from '../../types/SharedFilter'
3
+
4
+ describe('isUpdateNeeded', () => {
5
+ it('should return false when there are no filters and no query params', () => {
6
+ const filters: SharedFilter[] = []
7
+ const currentQueryParams = {}
8
+ const newQueryParams = {}
9
+ expect(isUpdateNeeded(filters, currentQueryParams, newQueryParams)).toBe(false)
10
+ })
11
+
12
+ it('should return true when there is a filter with type "urlfilter" and filterBy "File Name"', () => {
13
+ const filters: SharedFilter[] = [{ type: 'urlfilter', active: 'someValue', filterBy: 'File Name' }]
14
+ const currentQueryParams = {}
15
+ const newQueryParams = {}
16
+ expect(isUpdateNeeded(filters, currentQueryParams, newQueryParams)).toBe(true)
17
+ })
18
+
19
+ it('should return true when query params are different', () => {
20
+ const filters: SharedFilter[] = []
21
+ const currentQueryParams = { param1: 'value1' }
22
+ const newQueryParams = { param1: 'value2' }
23
+ expect(isUpdateNeeded(filters, currentQueryParams, newQueryParams)).toBe(true)
24
+ })
25
+
26
+ it('should return false when query params are the same', () => {
27
+ const filters: SharedFilter[] = []
28
+ const currentQueryParams = { param1: 'value1' }
29
+ const newQueryParams = { param1: 'value1' }
30
+ expect(isUpdateNeeded(filters, currentQueryParams, newQueryParams)).toBe(false)
31
+ })
32
+
33
+ it('should return false when filter type is "urlfilter" but filterBy is not "File Name"', () => {
34
+ const filters: SharedFilter[] = [{ type: 'urlfilter', active: 'someValue', filterBy: 'Other' }]
35
+ const currentQueryParams = {}
36
+ const newQueryParams = {}
37
+ expect(isUpdateNeeded(filters, currentQueryParams, newQueryParams)).toBe(false)
38
+ })
39
+
40
+ it('should return false when filter type is "urlfilter" and filterBy is "File Name" but active is an array', () => {
41
+ const filters: SharedFilter[] = [{ type: 'urlfilter', active: ['someValue'], filterBy: 'File Name' }]
42
+ const currentQueryParams = {}
43
+ const newQueryParams = {}
44
+ expect(isUpdateNeeded(filters, currentQueryParams, newQueryParams)).toBe(false)
45
+ })
46
+ })
47
+
48
+ describe('getDataURL', () => {
49
+ it('should return the base URL when there are no query parameters and no new file name', () => {
50
+ const updatedQSParams = {}
51
+ const dataUrl = new URL('https://example.com/path/to/file.csv')
52
+ const newFileName = ''
53
+ expect(getDataURL(updatedQSParams, dataUrl, newFileName)).toBe('https://example.com/path/to/file.csv')
54
+ })
55
+
56
+ it('should append query parameters correctly when they are strings and there is no new file name', () => {
57
+ const updatedQSParams = { param1: 'value1', param2: 'value2' }
58
+ const dataUrl = new URL('https://example.com/path/to/file.csv')
59
+ const newFileName = ''
60
+ expect(getDataURL(updatedQSParams, dataUrl, newFileName)).toBe('https://example.com/path/to/file.csv?param1="value1"&param2="value2"')
61
+ })
62
+
63
+ it('should append query parameters correctly when they are arrays and there is no new file name', () => {
64
+ const updatedQSParams = { param1: ['value1', 'value2'], param2: 'value3' }
65
+ const dataUrl = new URL('https://example.com/path/to/file.csv')
66
+ const newFileName = ''
67
+ expect(getDataURL(updatedQSParams, dataUrl, newFileName)).toBe('https://example.com/path/to/file.csv?param1="value1"&param1="value2"&param2="value3"')
68
+ })
69
+
70
+ it('should change the file name correctly when there are no query parameters but there is a new file name', () => {
71
+ const updatedQSParams = {}
72
+ const dataUrl = new URL('https://example.com/path/to/file.csv')
73
+ const newFileName = 'newfile'
74
+ expect(getDataURL(updatedQSParams, dataUrl, newFileName)).toBe('https://example.com/path/to/newfile.csv')
75
+ })
76
+
77
+ it('should change the file name and append query parameters correctly when they are strings and there is a new file name', () => {
78
+ const updatedQSParams = { param1: 'value1', param2: 'value2' }
79
+ const dataUrl = new URL('https://example.com/path/to/file.csv')
80
+ const newFileName = 'newfile'
81
+ expect(getDataURL(updatedQSParams, dataUrl, newFileName)).toBe('https://example.com/path/to/newfile.csv?param1="value1"&param2="value2"')
82
+ })
83
+
84
+ it('should change the file name and append query parameters correctly when they are arrays and there is a new file name', () => {
85
+ const updatedQSParams = { param1: ['value1', 'value2'], param2: 'value3' }
86
+ const dataUrl = new URL('https://example.com/path/to/file.csv')
87
+ const newFileName = 'newfile'
88
+ expect(getDataURL(updatedQSParams, dataUrl, newFileName)).toBe('https://example.com/path/to/newfile.csv?param1="value1"&param1="value2"&param2="value3"')
89
+ })
90
+ })
91
+
92
+ describe('getNewFileName', () => {
93
+ it('should return the formatted fileName when filter matches datasetKey and has a fileName', () => {
94
+ const newFileName = 'defaultFile'
95
+ const filter = {
96
+ datasetKey: 'dataset1',
97
+ fileName: 'state_${query}',
98
+ active: 'activeFilter',
99
+ whitespaceReplacement: 'Replace With Underscore'
100
+ }
101
+ const datasetKey = 'dataset1'
102
+ expect(getNewFileName(newFileName, filter, datasetKey)).toBe('State_ActiveFilter')
103
+ })
104
+
105
+ it('should return the active filter when filter matches datasetKey and does not have a fileName', () => {
106
+ const newFileName = 'defaultFile'
107
+ const filter = {
108
+ datasetKey: 'dataset1',
109
+ active: 'activeFilter',
110
+ whitespaceReplacement: 'Keep Spaces'
111
+ }
112
+ const datasetKey = 'dataset1'
113
+ expect(getNewFileName(newFileName, filter, datasetKey)).toBe('activeFilter')
114
+ })
115
+
116
+ it('should return the newFileName as is when filter does not match datasetKey', () => {
117
+ const newFileName = 'defaultFile'
118
+ const filter = {
119
+ datasetKey: 'dataset2',
120
+ fileName: 'state_${query}',
121
+ active: 'activeFilter',
122
+ whitespaceReplacement: 'Replace With Underscore'
123
+ }
124
+ const datasetKey = 'dataset1'
125
+ expect(getNewFileName(newFileName, filter, datasetKey)).toBe('defaultFile')
126
+ })
127
+
128
+ it('should replace ${query} with the active filter when filter matches datasetKey, has a fileName, and includes ${query}', () => {
129
+ const newFileName = 'defaultFile'
130
+ const filter = {
131
+ datasetKey: 'dataset1',
132
+ fileName: 'state_${query}',
133
+ active: 'activeFilter',
134
+ whitespaceReplacement: 'Keep Spaces'
135
+ }
136
+ const datasetKey = 'dataset1'
137
+ expect(getNewFileName(newFileName, filter, datasetKey)).toBe('State_ActiveFilter')
138
+ })
139
+
140
+ it('should handle whitespace replacement options correctly', () => {
141
+ const newFileName = 'defaultFile'
142
+ const filter = {
143
+ datasetKey: 'dataset1',
144
+ fileName: 'state_${query}',
145
+ active: 'active Filter',
146
+ whitespaceReplacement: 'Remove Spaces'
147
+ }
148
+ const datasetKey = 'dataset1'
149
+ expect(getNewFileName(newFileName, filter, datasetKey)).toBe('State_ActiveFilter')
150
+
151
+ filter.whitespaceReplacement = 'Keep Spaces'
152
+ expect(getNewFileName(newFileName, filter, datasetKey)).toBe('State_Active Filter')
153
+
154
+ filter.whitespaceReplacement = 'Replace With Underscore'
155
+ expect(getNewFileName(newFileName, filter, datasetKey)).toBe('State_Active_Filter')
156
+ })
157
+ })
158
+
159
+ describe('filterUsedByDataUrl', () => {
160
+ const visualizations = {
161
+ viz1: { dataKey: 'dataset1' },
162
+ viz2: { dataKey: 'dataset2' },
163
+ viz3: { dataKey: 'dataset1' }
164
+ }
165
+
166
+ it('should return true when filter has no usedBy property', () => {
167
+ const filter = { datasetKey: 'dataset1' }
168
+ const datasetKey = 'dataset1'
169
+ expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(true)
170
+ })
171
+
172
+ it('should return true when filter has an empty usedBy array', () => {
173
+ const filter = { usedBy: [], datasetKey: 'dataset1' }
174
+ const datasetKey = 'dataset1'
175
+ expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(true)
176
+ })
177
+
178
+ it('should return true when filter has usedBy array with visualization keys that match the datasetKey', () => {
179
+ const filter = { usedBy: ['viz1', 'viz3'], datasetKey: 'dataset1' }
180
+ const datasetKey = 'dataset1'
181
+ expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(true)
182
+ })
183
+
184
+ it('should return false when filter has usedBy array with visualization keys that do not match the datasetKey', () => {
185
+ const filter = { usedBy: ['viz2'], datasetKey: 'dataset1' }
186
+ const datasetKey = 'dataset1'
187
+ expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(false)
188
+ })
189
+
190
+ it('should return true when filter has usedBy array with a mix of matching and non-matching visualization keys', () => {
191
+ const filter = { usedBy: ['viz1', 'viz2'], datasetKey: 'dataset1' }
192
+ const datasetKey = 'dataset1'
193
+ expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(true)
194
+ })
195
+ })
@@ -249,7 +249,7 @@
249
249
  }
250
250
  &.checkbox {
251
251
  display: flex;
252
- span {
252
+ span:not(.cove-icon) {
253
253
  display: inline;
254
254
  }
255
255
  input {
@@ -7,15 +7,6 @@ $red: #f74242;
7
7
  margin-left: calc($editorWidth + 1em);
8
8
  }
9
9
 
10
- .visualizations-panel {
11
- background-color: #fff;
12
- padding: 1em;
13
- width: $editorWidth;
14
- border-right: #c7c7c7 1px solid;
15
- z-index: 1;
16
- overflow-y: scroll;
17
- }
18
-
19
10
  .hidden.editor-panel + .builder-grid {
20
11
  margin-left: 0 !important;
21
12
  }
@@ -30,30 +21,16 @@ $red: #f74242;
30
21
  padding: 5em 3em 3em;
31
22
  }
32
23
 
33
- .column-container {
34
- display: flex;
35
- flex-flow: row;
36
- width: 100%;
37
- position: relative;
38
- padding: 2em 1em 1em;
39
- border: 1px solid #c2c2c2;
40
- transition: border 300ms cubic-bezier(0.16, 1, 0.3, 1);
41
- background-color: #f2f2f2;
42
- user-select: none;
43
-
44
- &.can-drop {
45
- border-color: $blue-light;
46
- }
47
- }
48
-
49
24
  .row-menu {
50
25
  display: flex;
51
26
  align-items: flex-start;
52
27
  transition: opacity 300ms cubic-bezier(0.16, 1, 0.3, 1);
53
28
  user-select: none;
54
- position: relative;
29
+ position: absolute;
55
30
  z-index: 1;
56
- bottom: -1px;
31
+ top: -27px;
32
+ width: 100%;
33
+ left: 0;
57
34
 
58
35
  > li:not(.spacer) + li:not(.spacer) {
59
36
  margin-left: 0.3em;
@@ -337,6 +314,35 @@ $red: #f74242;
337
314
  top: 0;
338
315
  }
339
316
 
317
+ .footnotes {
318
+ margin: 0.5rem;
319
+ margin-bottom: 0;
320
+ width: calc(100% - 1rem);
321
+ border: 1px solid #c2c2c2;
322
+ transition: background-color 300ms cubic-bezier(0.16, 1, 0.3, 1);
323
+ background-color: #c2c2c2;
324
+ &:hover {
325
+ border-color: $blue;
326
+ background-color: $blue;
327
+ }
328
+ }
329
+
330
+ padding: 2em 1em 1em;
331
+ border: 1px solid #c2c2c2;
332
+ transition: border 300ms cubic-bezier(0.16, 1, 0.3, 1);
333
+ background-color: #f2f2f2;
334
+ user-select: none;
335
+
336
+ &.can-drop {
337
+ border-color: $blue-light;
338
+ }
339
+
340
+ .column-container {
341
+ display: flex;
342
+ flex-flow: row;
343
+ width: 100%;
344
+ }
345
+
340
346
  .widget__content {
341
347
  padding: 0 2em;
342
348
 
@@ -369,5 +375,6 @@ $red: #f74242;
369
375
  .column-container {
370
376
  border-color: $blue;
371
377
  }
378
+ border-color: $blue;
372
379
  }
373
380
  }
@@ -156,6 +156,13 @@
156
156
  z-index: -1;
157
157
  position: relative;
158
158
  }
159
+
160
+ // Expand and Collapse Buttons for Multiviz Dashboard
161
+ &.expand-collapse-buttons {
162
+ background-color: var(--lightestGray);
163
+ border: 1px var(--lightGray) solid;
164
+ color: black;
165
+ }
159
166
  }
160
167
 
161
168
  .warning-icon {
@@ -181,6 +188,40 @@
181
188
  }
182
189
  }
183
190
 
191
+ .collapsable-multiviz-container {
192
+ position: relative;
193
+ border: $lightGray 1px solid;
194
+ clear: both;
195
+ margin-bottom: 20px;
196
+ .multi-visualiation-heading {
197
+ position: relative;
198
+ background: var(--lightestGray);
199
+ padding: 0.5em 0.7em;
200
+ cursor: pointer;
201
+ svg {
202
+ position: absolute;
203
+ height: 100%;
204
+ width: 15px;
205
+ top: 0;
206
+ right: 1em;
207
+ }
208
+
209
+ &:focus {
210
+ z-index: 2;
211
+ position: relative;
212
+ }
213
+ @include breakpoint(xs) {
214
+ font-size: $font-small + 0.2em;
215
+ }
216
+ }
217
+ .data-table-heading {
218
+ display: none;
219
+ }
220
+ .table-container {
221
+ margin: 0 1em;
222
+ }
223
+ }
224
+
184
225
  .dashboard-download-link {
185
226
  font-size: 14px;
186
227
  }
@@ -205,9 +246,6 @@
205
246
  margin-left: 0;
206
247
  margin-right: 0;
207
248
  }
208
- &.hidden-toggle {
209
- display: none;
210
- }
211
249
  }
212
250
 
213
251
  .dashboard-col-12 {
@@ -1 +1,5 @@
1
1
  $editorWidth: 350px;
2
+
3
+ :root {
4
+ --editorWidth: #{$editorWidth};
5
+ }
@@ -1,15 +1,20 @@
1
1
  import type { DashboardConfig as Config } from '../types/DashboardConfig'
2
2
  import { type Action } from '@cdc/core/types/Action'
3
3
  import { Tab } from '../types/Tab'
4
- import { ConfigureData } from '@cdc/core/types/ConfigureData'
5
4
  import { ConfigRow } from '../types/ConfigRow'
5
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
6
+ import Footnotes from '@cdc/core/types/Footnotes'
7
+ import { SharedFilter } from '../types/SharedFilter'
6
8
 
7
- type SET_CONFIG = Action<'SET_CONFIG', Config>
9
+ type ADD_FOOTNOTE = Action<'ADD_FOOTNOTE', { id: string; rowIndex: number; config: Footnotes }>
10
+ type APPLY_CONFIG = Action<'APPLY_CONFIG', [Config, Object?]>
11
+ type SET_CONFIG = Action<'SET_CONFIG', Partial<Config>>
8
12
  type UPDATE_CONFIG = Action<'UPDATE_CONFIG', [Config, Object?]>
9
- type SET_DATA = Action<'SET_DATA', Object>
13
+ type SET_DATA = Action<'SET_DATA', Record<string, any[]>>
10
14
  type SET_LOADING = Action<'SET_LOADING', boolean>
11
15
  type SET_PREVIEW = Action<'SET_PREVIEW', boolean>
12
16
  type SET_FILTERED_DATA = Action<'SET_FILTERED_DATA', Object>
17
+ type SET_SHARED_FILTERS = Action<'SET_SHARED_FILTERS', SharedFilter[]>
13
18
  type SET_TAB_SELECTED = Action<'SET_TAB_SELECTED', Tab>
14
19
  type RENAME_DASHBOARD_TAB = Action<'RENAME_DASHBOARD_TAB', { current: string; new: string }>
15
20
  type INITIALIZE_MULTIDASHBOARDS = Action<'INITIALIZE_MULTIDASHBOARDS', undefined>
@@ -19,10 +24,12 @@ type ADD_NEW_DASHBOARD = Action<'ADD_NEW_DASHBOARD', undefined>
19
24
  type SAVE_CURRENT_CHANGES = Action<'SAVE_CURRENT_CHANGES', undefined>
20
25
  type SWITCH_CONFIG = Action<'SWITCH_CONFIG', number>
21
26
  type TOGGLE_ROW = Action<'TOGGLE_ROW', { rowIndex: number; colIndex: number }>
22
- type UPDATE_VISUALIZATION = Action<'UPDATE_VISUALIZATION', { vizKey: string; configureData: Partial<ConfigureData> }>
27
+ type UPDATE_VISUALIZATION = Action<'UPDATE_VISUALIZATION', { vizKey: string; configureData: Partial<AnyVisualization> }>
23
28
  type UPDATE_ROW = Action<'UPDATE_ROW', { rowIndex: number; rowData: Partial<ConfigRow> }>
24
29
 
25
30
  type DashboardActions =
31
+ | ADD_FOOTNOTE
32
+ | APPLY_CONFIG
26
33
  | ADD_NEW_DASHBOARD
27
34
  | SET_CONFIG
28
35
  | UPDATE_CONFIG
@@ -34,6 +41,7 @@ type DashboardActions =
34
41
  | SET_LOADING
35
42
  | SET_PREVIEW
36
43
  | SET_FILTERED_DATA
44
+ | SET_SHARED_FILTERS
37
45
  | SET_TAB_SELECTED
38
46
  | SWITCH_CONFIG
39
47
  | INITIALIZE_MULTIDASHBOARDS