@cdc/dashboard 4.24.7 → 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 +128394 -122305
- 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 +18 -6
- package/package.json +9 -9
- package/src/CdcDashboardComponent.tsx +154 -90
- package/src/DashboardContext.tsx +7 -1
- package/src/_stories/Dashboard.stories.tsx +124 -10
- package/src/_stories/_mock/api-filter-map.json +1 -1
- 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 +1 -1
- package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +39 -17
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +2 -2
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +141 -31
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +66 -18
- package/src/components/Header/Header.tsx +0 -5
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +20 -8
- package/src/components/Row.tsx +1 -1
- package/src/components/VisualizationRow.tsx +98 -17
- package/src/components/Widget.tsx +1 -0
- package/src/helpers/FilterBehavior.ts +4 -0
- package/src/helpers/addValuesToDashboardFilters.ts +49 -0
- package/src/helpers/apiFilterHelpers.ts +69 -18
- package/src/helpers/changeFilterActive.ts +16 -7
- package/src/helpers/getFilteredData.ts +4 -4
- package/src/helpers/iconHash.tsx +2 -0
- package/src/helpers/loadAPIFilters.ts +74 -0
- package/src/helpers/reloadURLHelpers.ts +13 -3
- package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
- 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/types/SharedFilter.ts +2 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { setAutoLoadDefaultValue, getToFetch, getFilterValues, getLoadingFilterMemo } from '../apiFilterHelpers'
|
|
2
|
+
import _ from 'lodash'
|
|
3
|
+
import type { APIFilterDropdowns } from '../../components/DashboardFilters'
|
|
4
|
+
import type { SharedFilter } from '../../types/SharedFilter'
|
|
5
|
+
|
|
6
|
+
describe('getLoadingFilterMemo', () => {
|
|
7
|
+
it('should return correct APIFilterDropdowns for valid inputs', () => {
|
|
8
|
+
const sharedAPIFilters = ['endpoint1', 'endpoint2']
|
|
9
|
+
const apiFilterDropdowns: APIFilterDropdowns = {
|
|
10
|
+
endpoint1: { text: 'text1', value: 'value1' }
|
|
11
|
+
}
|
|
12
|
+
const expectedOutput: APIFilterDropdowns = {
|
|
13
|
+
endpoint1: { text: 'text1', value: 'value1' },
|
|
14
|
+
endpoint2: null
|
|
15
|
+
}
|
|
16
|
+
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return an empty object for empty sharedAPIFilters', () => {
|
|
20
|
+
const sharedAPIFilters: string[] = []
|
|
21
|
+
const apiFilterDropdowns: APIFilterDropdowns = {
|
|
22
|
+
endpoint1: { text: 'text1', value: 'value1' }
|
|
23
|
+
}
|
|
24
|
+
const expectedOutput: APIFilterDropdowns = {}
|
|
25
|
+
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should return APIFilterDropdowns with null values for empty apiFilterDropdowns', () => {
|
|
29
|
+
const sharedAPIFilters = ['endpoint1', 'endpoint2']
|
|
30
|
+
const apiFilterDropdowns: APIFilterDropdowns = {}
|
|
31
|
+
const expectedOutput: APIFilterDropdowns = {
|
|
32
|
+
endpoint1: null,
|
|
33
|
+
endpoint2: null
|
|
34
|
+
}
|
|
35
|
+
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should not overwrite existing data in apiFilterDropdowns, so return original dropdowns', () => {
|
|
39
|
+
const sharedAPIFilters = ['endpoint1', 'endpoint2']
|
|
40
|
+
const apiFilterDropdowns: APIFilterDropdowns = {
|
|
41
|
+
endpoint1: { text: 'text1', value: 'value1' },
|
|
42
|
+
endpoint2: { text: 'text2', value: 'value2' }
|
|
43
|
+
}
|
|
44
|
+
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(apiFilterDropdowns)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('getFilterValues', () => {
|
|
49
|
+
it('should return correct filter values for valid inputs', () => {
|
|
50
|
+
const data = [{ key1: 'value1', key2: 'value2' }]
|
|
51
|
+
const apiFilter = { textSelector: 'key1', valueSelector: 'key2' }
|
|
52
|
+
const expectedOutput = [{ text: 'value1', value: 'value2' }]
|
|
53
|
+
expect(getFilterValues(data, apiFilter)).toEqual(expectedOutput)
|
|
54
|
+
delete apiFilter.textSelector
|
|
55
|
+
const expectedOutput2 = [{ text: 'value2', value: 'value2' }]
|
|
56
|
+
expect(getFilterValues(data, apiFilter)).toEqual(expectedOutput2)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('getToFetch', () => {
|
|
61
|
+
it('should return an empty object when sharedAPIFilters is empty', () => {
|
|
62
|
+
const result = getToFetch([], {})
|
|
63
|
+
expect(result).toEqual({})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should return an object with endpoints when apiFilterDropdowns is empty', () => {
|
|
67
|
+
const sharedAPIFilters = [{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] }]
|
|
68
|
+
const result = getToFetch(sharedAPIFilters, {})
|
|
69
|
+
expect(result).toEqual({ '/endpoint1': ['/endpoint1', 0] })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should return and empty object when sharedAPIFilters contains filters with no parents', () => {
|
|
73
|
+
const sharedAPIFilters = [{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] }]
|
|
74
|
+
const apiFilterDropdowns = { '/endpoint1': true }
|
|
75
|
+
const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
|
|
76
|
+
expect(result).toEqual({})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should return an empty object when parentParams contains an empty value', () => {
|
|
80
|
+
const sharedAPIFilters = [
|
|
81
|
+
{ key: 'parent1', apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] },
|
|
82
|
+
{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: ['parent1'] }
|
|
83
|
+
]
|
|
84
|
+
const apiFilterDropdowns = { '/endpoint1': true }
|
|
85
|
+
const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
|
|
86
|
+
expect(result).toEqual({})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should return an empty object when parentParams contains an empty value', () => {
|
|
90
|
+
const sharedAPIFilters = [
|
|
91
|
+
{ key: 'parent1', value: '', apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] },
|
|
92
|
+
{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: ['parent1'] }
|
|
93
|
+
]
|
|
94
|
+
const apiFilterDropdowns = { '/endpoint1': true }
|
|
95
|
+
const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
|
|
96
|
+
expect(result).toEqual({})
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('setAutoLoadDefaultValue', () => {
|
|
101
|
+
const dropdownOptions = [
|
|
102
|
+
{ value: 'option1', label: 'Option 1' },
|
|
103
|
+
{ value: 'option2', label: 'Option 2' }
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const sharedFilters = [
|
|
107
|
+
{ key: 'filter1', active: null, queuedActive: null, parents: [] },
|
|
108
|
+
{ key: 'filter2', active: null, queuedActive: null, parents: ['filter1'] }
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
it('should return the original filter when autoLoadFilterIndexes is empty', () => {
|
|
112
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFilters, [])
|
|
113
|
+
expect(result).toEqual(sharedFilters[0])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should return the original filter when dropdownOptions is empty', () => {
|
|
117
|
+
const result = setAutoLoadDefaultValue(0, [], sharedFilters, [0])
|
|
118
|
+
expect(result).toEqual(sharedFilters[0])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should return the original filter when dropdownOptions is undefined', () => {
|
|
122
|
+
const result = setAutoLoadDefaultValue(0, undefined, sharedFilters, [0])
|
|
123
|
+
expect(result).toEqual(sharedFilters[0])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should return the original filter when sharedFilterIndex is not in autoLoadFilterIndexes', () => {
|
|
127
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFilters, [1])
|
|
128
|
+
expect(result).toEqual(sharedFilters[0])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should return the original filter when not all parent filters are selected', () => {
|
|
132
|
+
const result = setAutoLoadDefaultValue(1, dropdownOptions, sharedFilters, [1])
|
|
133
|
+
expect(result).toEqual(sharedFilters[1])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should assign the default value from dropdownOptions when no active value is set', () => {
|
|
137
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
138
|
+
sharedFiltersCopy[0].active = null
|
|
139
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
|
|
140
|
+
expect(result.active).toEqual('option1')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should retain the current active value if it exists in dropdownOptions', () => {
|
|
144
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
145
|
+
sharedFiltersCopy[0].active = 'option1'
|
|
146
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
|
|
147
|
+
expect(result.active).toEqual('option1')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should assign the default value if the current active value does not exist in dropdownOptions', () => {
|
|
151
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
152
|
+
sharedFiltersCopy[0].active = 'nonexistent'
|
|
153
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
|
|
154
|
+
expect(result.active).toEqual('option1')
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -7,13 +7,14 @@ export type SharedFilter = FilterBase & {
|
|
|
7
7
|
queryParameter?: string
|
|
8
8
|
setByQueryParameter?: string
|
|
9
9
|
active?: string | string[]
|
|
10
|
-
queuedActive?: string
|
|
10
|
+
queuedActive?: string | string[]
|
|
11
11
|
usedBy?: (string | number)[] // if number used by whole row, else used by specific viz
|
|
12
12
|
parents?: string[]
|
|
13
13
|
setBy?: string
|
|
14
14
|
selectLimit?: number
|
|
15
15
|
resetLabel?: string
|
|
16
16
|
labels?: Record<string, any>
|
|
17
|
+
multiSelect?: boolean
|
|
17
18
|
key: string
|
|
18
19
|
apiFilter?: APIFilter
|
|
19
20
|
datasetKey?: string
|