@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.
- package/dist/cdcdashboard.js +144406 -127510
- package/examples/custom/css/respiratory.css +236 -0
- package/examples/custom/js/respiratory.js +242 -0
- package/examples/default-multi-dataset-shared-filter.json +1729 -0
- package/examples/ed-visits-county-file.json +618 -0
- package/examples/filtered-dash.json +6 -21
- package/examples/single-state-dashboard-filters.json +421 -0
- package/examples/state-level.json +90136 -0
- package/examples/state-points.json +10474 -0
- package/examples/test-file.json +147 -0
- package/examples/testing.json +94456 -0
- package/index.html +25 -4
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +250 -327
- package/src/DashboardContext.tsx +15 -1
- package/src/_stories/Dashboard.stories.tsx +158 -40
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/bump-chart.json +3554 -0
- package/src/_stories/_mock/methodology.json +412 -0
- package/src/_stories/_mock/methodologyAPI.ts +90 -0
- package/src/_stories/_mock/multi-viz.json +3 -4
- package/src/_stories/_mock/pivot-filter.json +14 -12
- package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
- package/src/components/CollapsibleVisualizationRow.tsx +44 -0
- package/src/components/Column.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
- package/src/components/DashboardFilters/index.ts +3 -0
- package/src/components/DataDesignerModal.tsx +9 -9
- package/src/components/ExpandCollapseButtons.tsx +20 -0
- package/src/components/Header/Header.tsx +1 -102
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
- package/src/components/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +169 -30
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
- package/src/components/VisualizationsPanel/index.ts +1 -0
- package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
- package/src/components/Widget.tsx +27 -90
- package/src/helpers/FilterBehavior.ts +4 -0
- package/src/helpers/addValuesToDashboardFilters.ts +49 -0
- package/src/helpers/apiFilterHelpers.ts +102 -0
- package/src/helpers/changeFilterActive.ts +39 -0
- package/src/helpers/filterData.ts +10 -48
- package/src/helpers/generateValuesForFilter.ts +1 -1
- package/src/helpers/getAutoLoadVisualization.ts +11 -0
- package/src/helpers/getFilteredData.ts +7 -5
- package/src/helpers/getVizConfig.ts +23 -2
- package/src/helpers/getVizRowColumnLocator.ts +2 -1
- package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
- package/src/helpers/iconHash.tsx +5 -3
- package/src/helpers/loadAPIFilters.ts +74 -0
- package/src/helpers/mapDataToConfig.ts +29 -0
- package/src/helpers/processData.ts +2 -3
- package/src/helpers/reloadURLHelpers.ts +78 -0
- package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
- package/src/helpers/tests/filterData.test.ts +1 -93
- package/src/helpers/tests/getFilteredData.test.ts +86 -0
- package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +176 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +195 -0
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/grid.scss +34 -27
- package/src/scss/main.scss +41 -3
- package/src/scss/variables.scss +4 -0
- package/src/store/dashboard.actions.ts +12 -4
- package/src/store/dashboard.reducer.ts +30 -4
- package/src/types/APIFilter.ts +1 -5
- package/src/types/ConfigRow.ts +2 -0
- package/src/types/Dashboard.ts +1 -1
- package/src/types/DashboardConfig.ts +2 -4
- package/src/types/DashboardFilters.ts +7 -0
- package/src/types/InitialState.ts +1 -1
- package/src/types/MultiDashboard.ts +2 -2
- package/src/types/SharedFilter.ts +4 -6
- package/src/types/Tab.ts +1 -1
- package/LICENSE +0 -201
- package/src/components/Filters.tsx +0 -88
- package/src/components/Header/FilterModal.tsx +0 -510
- package/src/components/VisualizationsPanel.tsx +0 -95
- 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"¶m2="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"¶m1="value2"¶m2="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"¶m2="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"¶m1="value2"¶m2="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
|
+
})
|
package/src/scss/grid.scss
CHANGED
|
@@ -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:
|
|
29
|
+
position: absolute;
|
|
55
30
|
z-index: 1;
|
|
56
|
-
|
|
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
|
}
|
package/src/scss/main.scss
CHANGED
|
@@ -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 {
|
package/src/scss/variables.scss
CHANGED
|
@@ -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
|
|
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',
|
|
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<
|
|
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
|