@cdc/dashboard 4.24.10 → 4.24.12-2
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 +51165 -49100
- package/examples/ed-visits-county-file.json +141 -357
- package/examples/private/DEV-10120.json +1294 -0
- package/examples/private/DEV-9199.json +606 -0
- package/examples/private/DEV-9644.json +20092 -0
- package/examples/private/DEV-9684.json +2135 -0
- package/examples/private/DEV-9989.json +229 -0
- package/examples/private/art-dashboard.json +18174 -0
- package/examples/private/art-scratch.json +2406 -0
- package/examples/private/dashboard-config-ehdi.json +29915 -0
- package/examples/private/dashboard-margins.js +15 -0
- package/examples/private/dataset.json +1452 -0
- package/examples/private/ehdi-data.json +29502 -0
- package/examples/private/gaza-issue.json +1214 -0
- package/examples/private/workforce.json +2041 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +43 -29
- package/src/CdcDashboardComponent.tsx +91 -52
- package/src/DashboardContext.tsx +2 -0
- package/src/_stories/Dashboard.stories.tsx +8 -0
- package/src/_stories/_mock/api-filter-error.json +55 -0
- package/src/_stories/_mock/group-pivot-filter.json +10 -5
- package/src/components/CollapsibleVisualizationRow.tsx +8 -2
- package/src/components/DashboardFilters/DashboardFilters.tsx +121 -58
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +3 -1
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +54 -50
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +13 -7
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +21 -0
- package/src/components/DashboardFilters/dashboardfilter.styles.css +27 -0
- package/src/components/Grid.tsx +1 -1
- package/src/components/Header/Header.tsx +71 -10
- package/src/components/Header/index.scss +0 -5
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +28 -6
- package/src/components/MultiConfigTabs/MultiTabs.tsx +2 -0
- package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +4 -11
- package/src/components/Row.tsx +59 -13
- package/src/components/VisualizationRow.tsx +30 -22
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +0 -1
- package/src/components/Widget.tsx +23 -1
- package/src/data/initial-state.js +2 -1
- package/src/helpers/addValuesToDashboardFilters.ts +4 -2
- package/src/helpers/apiFilterHelpers.ts +55 -20
- package/src/helpers/changeFilterActive.ts +3 -0
- package/src/helpers/filterData.ts +1 -1
- package/src/helpers/getVizRowColumnLocator.ts +1 -0
- package/src/helpers/loadAPIFilters.ts +32 -10
- package/src/helpers/reloadURLHelpers.ts +9 -2
- package/src/helpers/shouldLoadAllFilters.ts +30 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +85 -4
- package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +10 -4
- package/src/helpers/tests/reloadURLHelpers.test.ts +11 -5
- package/src/helpers/tests/shouldLoadAllFilters.test.ts +117 -0
- package/src/scss/editor-panel.scss +0 -3
- package/src/scss/grid.scss +22 -23
- package/src/scss/main.scss +0 -27
- package/src/store/dashboard.reducer.ts +9 -2
- package/src/store/errorMessage/errorMessage.actions.ts +7 -0
- package/src/store/errorMessage/errorMessage.reducer.ts +24 -0
|
@@ -3,29 +3,41 @@ import { APIFilterDropdowns } from '../components/DashboardFilters'
|
|
|
3
3
|
import { SharedFilter } from '../types/SharedFilter'
|
|
4
4
|
import * as apiFilterHelpers from './apiFilterHelpers'
|
|
5
5
|
import { APIFilter } from '../types/APIFilter'
|
|
6
|
+
import { getParentParams, notAllParentsSelected } from './apiFilterHelpers'
|
|
6
7
|
|
|
7
8
|
export const loadAPIFiltersFactory = (
|
|
8
9
|
dispatch: Function,
|
|
10
|
+
dispatchErrorMessages: Function,
|
|
9
11
|
setAPIFilterDropdowns: Function,
|
|
10
12
|
autoLoadFilterIndexes: number[]
|
|
11
13
|
) => {
|
|
12
14
|
const loadAPIFilters = (
|
|
13
15
|
sharedFilters: SharedFilter[],
|
|
14
16
|
dropdowns: APIFilterDropdowns,
|
|
15
|
-
|
|
17
|
+
loadAll?: boolean,
|
|
18
|
+
recursiveLimit = 50
|
|
16
19
|
): Promise<SharedFilter[]> => {
|
|
17
20
|
if (!sharedFilters) return
|
|
21
|
+
const allIndexes = sharedFilters.map((_, index) => index)
|
|
22
|
+
const _autoLoadFilterIndexes = loadAll ? allIndexes : autoLoadFilterIndexes
|
|
18
23
|
sharedFilters = sharedFilters.map((filter, index) =>
|
|
19
24
|
apiFilterHelpers.setAutoLoadDefaultValue(
|
|
20
25
|
index,
|
|
21
26
|
dropdowns[filter.apiFilter?.apiEndpoint],
|
|
22
27
|
sharedFilters,
|
|
23
|
-
|
|
28
|
+
_autoLoadFilterIndexes
|
|
24
29
|
)
|
|
25
30
|
)
|
|
26
31
|
const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
|
|
27
32
|
const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
|
|
28
33
|
const toFetch = apiFilterHelpers.getToFetch(sharedFilters, dropdowns)
|
|
34
|
+
const loadingDropdowns = Object.values(toFetch).reduce(
|
|
35
|
+
(acc, [dropdownsKey]) => ({ ...acc, [dropdownsKey]: null }),
|
|
36
|
+
{}
|
|
37
|
+
)
|
|
38
|
+
setAPIFilterDropdowns(currentState => {
|
|
39
|
+
return { ...currentState, ...loadingDropdowns }
|
|
40
|
+
})
|
|
29
41
|
const newDropdowns = _.cloneDeep(dropdowns)
|
|
30
42
|
return Promise.all(
|
|
31
43
|
Object.keys(toFetch).map(
|
|
@@ -36,7 +48,6 @@ export const loadAPIFiltersFactory = (
|
|
|
36
48
|
.then(data => {
|
|
37
49
|
if (!Array.isArray(data)) {
|
|
38
50
|
console.error('COVE only supports response data in the shape Array<Object>')
|
|
39
|
-
return
|
|
40
51
|
}
|
|
41
52
|
const [_key, index] = toFetch[endpoint]
|
|
42
53
|
const apiFilter = filterLookup.get(_key) as APIFilter
|
|
@@ -47,27 +58,38 @@ export const loadAPIFiltersFactory = (
|
|
|
47
58
|
index,
|
|
48
59
|
_filterValues,
|
|
49
60
|
sharedFilters,
|
|
50
|
-
|
|
61
|
+
_autoLoadFilterIndexes
|
|
51
62
|
)
|
|
52
63
|
sharedFilters[index] = newDefaultSelectedFilter
|
|
53
64
|
})
|
|
54
|
-
.catch(
|
|
65
|
+
.catch(() => {
|
|
66
|
+
dispatchErrorMessages({
|
|
67
|
+
type: 'ADD_ERROR_MESSAGE',
|
|
68
|
+
payload: 'There was a problem returning data. Please try again.'
|
|
69
|
+
})
|
|
70
|
+
})
|
|
55
71
|
.finally(() => {
|
|
56
72
|
resolve()
|
|
57
73
|
})
|
|
58
74
|
})
|
|
59
75
|
)
|
|
60
76
|
).then(() => {
|
|
61
|
-
const
|
|
62
|
-
|
|
77
|
+
const toLoad = sharedFilters.reduce((acc, curr, index) => {
|
|
78
|
+
// the filter is autoloading and it hasn't finished yet
|
|
79
|
+
if (_autoLoadFilterIndexes.includes(index) && !curr.active) {
|
|
80
|
+
if (notAllParentsSelected(getParentParams(curr, sharedFilters))) {
|
|
81
|
+
return acc
|
|
82
|
+
}
|
|
83
|
+
return [...acc, index]
|
|
84
|
+
}
|
|
63
85
|
return acc
|
|
64
|
-
},
|
|
65
|
-
if (
|
|
86
|
+
}, [])
|
|
87
|
+
if (!toLoad.length || recursiveLimit === 0) {
|
|
66
88
|
setAPIFilterDropdowns(newDropdowns)
|
|
67
89
|
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
|
|
68
90
|
return sharedFilters
|
|
69
91
|
} else {
|
|
70
|
-
return loadAPIFilters(sharedFilters, newDropdowns, recursiveLimit - 1)
|
|
92
|
+
return loadAPIFilters(sharedFilters, newDropdowns, loadAll, recursiveLimit - 1)
|
|
71
93
|
}
|
|
72
94
|
})
|
|
73
95
|
}
|
|
@@ -4,6 +4,7 @@ import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
|
|
|
4
4
|
import { AnyVisualization, Visualization } from '@cdc/core/types/Visualization'
|
|
5
5
|
import _ from 'lodash'
|
|
6
6
|
import { DashboardConfig } from '../types/DashboardConfig'
|
|
7
|
+
import { ConfigRow } from '../types/ConfigRow'
|
|
7
8
|
|
|
8
9
|
export const isUpdateNeeded = (
|
|
9
10
|
filters: SharedFilter[],
|
|
@@ -104,9 +105,15 @@ export const getVisualizationsWithFormattedData = (visualizations: Record<string
|
|
|
104
105
|
export const filterUsedByDataUrl = (
|
|
105
106
|
filter: SharedFilter,
|
|
106
107
|
datasetKey: string,
|
|
107
|
-
visualizations: Record<string, AnyVisualization
|
|
108
|
+
visualizations: Record<string, AnyVisualization>,
|
|
109
|
+
rows: ConfigRow[]
|
|
108
110
|
) => {
|
|
109
111
|
if (!filter.usedBy || !filter.usedBy.length) return true
|
|
110
|
-
const vizUsingFilters = filter.usedBy?.map(
|
|
112
|
+
const vizUsingFilters = filter.usedBy?.map(vizOrRowKey => visualizations[vizOrRowKey] || rows[vizOrRowKey])
|
|
113
|
+
// push any footnotes which are using the filter also
|
|
114
|
+
filter.usedBy?.forEach(vizOrRowKey => {
|
|
115
|
+
if (rows[vizOrRowKey] && rows[vizOrRowKey].footnotesId)
|
|
116
|
+
return vizUsingFilters.push(visualizations[rows[vizOrRowKey].footnotesId])
|
|
117
|
+
})
|
|
111
118
|
return vizUsingFilters?.some(viz => viz?.dataKey === datasetKey)
|
|
112
119
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getQueryParam } from '@cdc/core/helpers/queryStringUtils'
|
|
2
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
|
|
4
|
+
export const shouldLoadAllFilters = (config, isEditorPanel): boolean => {
|
|
5
|
+
const autoLoad = Boolean(getQueryParam('cove-auto-load'))
|
|
6
|
+
const activeConfig = config.multiDashboards ? config.multiDashboards[config.activeDashboard] : config
|
|
7
|
+
const hasFilterByFileNameFunctionality = activeConfig.dashboard.sharedFilters?.some(
|
|
8
|
+
filter => filter.filterBy === 'File Name'
|
|
9
|
+
)
|
|
10
|
+
const isAutoLoadTab = Object.values(activeConfig.visualizations).reduce((acc, viz: Visualization) => {
|
|
11
|
+
if (acc === false) return acc
|
|
12
|
+
if (viz.visualizationType === 'dashboardFilters') {
|
|
13
|
+
if (viz.filterBehavior === 'Apply Button') return false
|
|
14
|
+
if (viz.autoLoad) {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return acc
|
|
19
|
+
}, undefined)
|
|
20
|
+
if (autoLoad || isAutoLoadTab || hasFilterByFileNameFunctionality || isEditorPanel) {
|
|
21
|
+
const rowDataSetKeys = activeConfig.rows.map(row => row.dataKey).filter(Boolean)
|
|
22
|
+
const dataKeys = Object.values(activeConfig.visualizations)
|
|
23
|
+
.map((visualization: Visualization) => visualization.dataKey)
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.concat(rowDataSetKeys)
|
|
26
|
+
const missingData = dataKeys.find(dataset => !config.datasets[dataset].data?.length)
|
|
27
|
+
return Boolean(missingData)
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
getToFetch,
|
|
4
4
|
getFilterValues,
|
|
5
5
|
getLoadingFilterMemo,
|
|
6
|
-
getParentParams
|
|
6
|
+
getParentParams,
|
|
7
|
+
setActiveNestedDropdown,
|
|
8
|
+
setActiveMultiDropdown
|
|
7
9
|
} from '../apiFilterHelpers'
|
|
8
10
|
import _ from 'lodash'
|
|
9
11
|
import type { APIFilterDropdowns } from '../../components/DashboardFilters'
|
|
@@ -18,7 +20,7 @@ describe('getLoadingFilterMemo', () => {
|
|
|
18
20
|
}
|
|
19
21
|
const expectedOutput: APIFilterDropdowns = {
|
|
20
22
|
endpoint1: { text: 'text1', value: 'value1' },
|
|
21
|
-
endpoint2:
|
|
23
|
+
endpoint2: undefined
|
|
22
24
|
}
|
|
23
25
|
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
|
|
24
26
|
})
|
|
@@ -36,8 +38,8 @@ describe('getLoadingFilterMemo', () => {
|
|
|
36
38
|
const sharedAPIFilters = ['endpoint1', 'endpoint2']
|
|
37
39
|
const apiFilterDropdowns: APIFilterDropdowns = {}
|
|
38
40
|
const expectedOutput: APIFilterDropdowns = {
|
|
39
|
-
endpoint1:
|
|
40
|
-
endpoint2:
|
|
41
|
+
endpoint1: undefined,
|
|
42
|
+
endpoint2: undefined
|
|
41
43
|
}
|
|
42
44
|
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
|
|
43
45
|
})
|
|
@@ -280,6 +282,85 @@ describe('getToFetch', () => {
|
|
|
280
282
|
})
|
|
281
283
|
})
|
|
282
284
|
|
|
285
|
+
describe('setActiveNestedDropdown', () => {
|
|
286
|
+
const dropdownOptions = [
|
|
287
|
+
{ value: 'option1', subOptions: [{ value: 'subOption1' }], label: 'Option 1' },
|
|
288
|
+
{ value: 'option2', subOptions: [{ value: 'subOption2' }], label: 'Option 2' }
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
const sharedFilters = [
|
|
292
|
+
{
|
|
293
|
+
key: 'filter1',
|
|
294
|
+
active: null,
|
|
295
|
+
filterStyle: FILTER_STYLE.nestedDropdown,
|
|
296
|
+
subGrouping: {},
|
|
297
|
+
queuedActive: null,
|
|
298
|
+
parents: []
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
key: 'filter2',
|
|
302
|
+
active: null,
|
|
303
|
+
setByQueryParameter: 'group',
|
|
304
|
+
filterStyle: FILTER_STYLE.nestedDropdown,
|
|
305
|
+
subGrouping: { setByQueryParameter: 'subgroup' },
|
|
306
|
+
queuedActive: null,
|
|
307
|
+
parents: ['filter1']
|
|
308
|
+
}
|
|
309
|
+
] as SharedFilter[]
|
|
310
|
+
|
|
311
|
+
it('should set the active value for a nested dropdown', () => {
|
|
312
|
+
setActiveNestedDropdown(dropdownOptions, sharedFilters[0])
|
|
313
|
+
expect(sharedFilters[0].active).toEqual('option1')
|
|
314
|
+
expect(sharedFilters[0].subGrouping.active).toEqual('subOption1')
|
|
315
|
+
})
|
|
316
|
+
it('should set the active value for nested dropdown with query parameters', () => {
|
|
317
|
+
delete window.location
|
|
318
|
+
window.location = new URL('https://www.example.com?group=option2&subgroup=subOption2')
|
|
319
|
+
setActiveNestedDropdown(dropdownOptions, sharedFilters[1])
|
|
320
|
+
expect(sharedFilters[1].active).toEqual('option2')
|
|
321
|
+
expect(sharedFilters[1].subGrouping.active).toEqual('subOption2')
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe('setActiveMultiDropdown', () => {
|
|
326
|
+
const dropdownOptions = [
|
|
327
|
+
{ value: 'option1', label: 'Option 1' },
|
|
328
|
+
{ value: 'option2', label: 'Option 2' }
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
const sharedFilters = [
|
|
332
|
+
{
|
|
333
|
+
key: 'filter1',
|
|
334
|
+
active: null,
|
|
335
|
+
filterStyle: FILTER_STYLE.multiSelect,
|
|
336
|
+
queuedActive: null,
|
|
337
|
+
parents: []
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
key: 'filter2',
|
|
341
|
+
active: null,
|
|
342
|
+
filterStyle: FILTER_STYLE.multiSelect,
|
|
343
|
+
setByQueryParameter: 'group',
|
|
344
|
+
queuedActive: null,
|
|
345
|
+
parents: ['filter1']
|
|
346
|
+
}
|
|
347
|
+
] as SharedFilter[]
|
|
348
|
+
it('should set the active value for a multi dropdown', () => {
|
|
349
|
+
setActiveMultiDropdown(dropdownOptions, sharedFilters[0])
|
|
350
|
+
expect(sharedFilters[0].active).toEqual(['option1'])
|
|
351
|
+
})
|
|
352
|
+
it('should set the active value for a multi dropdown with queryParameters', () => {
|
|
353
|
+
delete window.location
|
|
354
|
+
window.location = new URL('https://www.example.com?group=option1&group=option2')
|
|
355
|
+
setActiveMultiDropdown(dropdownOptions, sharedFilters[1])
|
|
356
|
+
expect(sharedFilters[1].active).toEqual(['option1', 'option2'])
|
|
357
|
+
delete window.location
|
|
358
|
+
window.location = new URL('https://www.example.com?group=option1,option2')
|
|
359
|
+
setActiveMultiDropdown(dropdownOptions, sharedFilters[1])
|
|
360
|
+
expect(sharedFilters[1].active).toEqual(['option1', 'option2'])
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
283
364
|
describe('setAutoLoadDefaultValue', () => {
|
|
284
365
|
const dropdownOptions = [
|
|
285
366
|
{ value: 'option1', label: 'Option 1' },
|
|
@@ -66,6 +66,7 @@ global.fetch = fetch
|
|
|
66
66
|
|
|
67
67
|
describe('loadAPIFiltersFactory', () => {
|
|
68
68
|
const dispatch = vi.fn()
|
|
69
|
+
const dispatchErrorMessages = vi.fn()
|
|
69
70
|
const setAPIFilterDropdowns = vi.fn()
|
|
70
71
|
const apiFilterDropdowns = {
|
|
71
72
|
'cdc.gov/filters/Sex': [
|
|
@@ -76,7 +77,7 @@ describe('loadAPIFiltersFactory', () => {
|
|
|
76
77
|
afterEach(() => {
|
|
77
78
|
vi.restoreAllMocks()
|
|
78
79
|
})
|
|
79
|
-
const loadAPIFilters = loadAPIFiltersFactory(dispatch, setAPIFilterDropdowns, [2])
|
|
80
|
+
const loadAPIFilters = loadAPIFiltersFactory(dispatch, dispatchErrorMessages, setAPIFilterDropdowns, [2])
|
|
80
81
|
it('creates a function', () => {
|
|
81
82
|
expect(typeof loadAPIFilters).toEqual('function')
|
|
82
83
|
})
|
|
@@ -90,7 +91,7 @@ describe('loadAPIFiltersFactory', () => {
|
|
|
90
91
|
it('loadAPIFilters() load dropdowns for children when parent is selected', async () => {
|
|
91
92
|
const newSharedFilters = await loadAPIFilters(sharedFilters, apiFilterDropdowns)
|
|
92
93
|
expect(dispatch).toHaveBeenCalledTimes(1)
|
|
93
|
-
expect(setAPIFilterDropdowns).toHaveBeenCalledTimes(
|
|
94
|
+
expect(setAPIFilterDropdowns).toHaveBeenCalledTimes(2)
|
|
94
95
|
|
|
95
96
|
const expectedDropdowns = {
|
|
96
97
|
'cdc.gov/filters/Quarter': [
|
|
@@ -132,7 +133,7 @@ describe('loadAPIFiltersFactory', () => {
|
|
|
132
133
|
sharedFilters[1].active = 'Q1'
|
|
133
134
|
const newSharedFilters = await loadAPIFilters(sharedFilters, apiFilterDropdowns)
|
|
134
135
|
expect(dispatch).toHaveBeenCalledTimes(1)
|
|
135
|
-
expect(setAPIFilterDropdowns).toHaveBeenCalledTimes(
|
|
136
|
+
expect(setAPIFilterDropdowns).toHaveBeenCalledTimes(2)
|
|
136
137
|
|
|
137
138
|
const expectedDropdowns = {
|
|
138
139
|
'cdc.gov/filters/Quarter': [
|
|
@@ -213,8 +214,13 @@ describe('loadAPIFiltersFactory', () => {
|
|
|
213
214
|
}
|
|
214
215
|
const newSharedFilters = await loadAPIFilters(_sharedFilters, apiDropdownsLoaded)
|
|
215
216
|
expect(dispatch).toHaveBeenCalledTimes(1)
|
|
216
|
-
expect(setAPIFilterDropdowns).toHaveBeenCalledTimes(
|
|
217
|
+
expect(setAPIFilterDropdowns).toHaveBeenCalledTimes(2)
|
|
217
218
|
|
|
218
219
|
expect(newSharedFilters[2].active).toEqual(['2020', '2021'])
|
|
219
220
|
})
|
|
221
|
+
it('loads All filters if loadAll is true', async () => {
|
|
222
|
+
const _sharedFilters = _.cloneDeep(sharedFilters)
|
|
223
|
+
const newSharedFilters = await loadAPIFilters(_sharedFilters, {}, true)
|
|
224
|
+
expect(newSharedFilters[2].active).toEqual([2020])
|
|
225
|
+
})
|
|
220
226
|
})
|
|
@@ -203,30 +203,36 @@ describe('filterUsedByDataUrl', () => {
|
|
|
203
203
|
it('should return true when filter has no usedBy property', () => {
|
|
204
204
|
const filter = { datasetKey: 'dataset1' }
|
|
205
205
|
const datasetKey = 'dataset1'
|
|
206
|
-
expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(true)
|
|
206
|
+
expect(filterUsedByDataUrl(filter, datasetKey, visualizations, [])).toBe(true)
|
|
207
207
|
})
|
|
208
208
|
|
|
209
209
|
it('should return true when filter has an empty usedBy array', () => {
|
|
210
210
|
const filter = { usedBy: [], datasetKey: 'dataset1' }
|
|
211
211
|
const datasetKey = 'dataset1'
|
|
212
|
-
expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(true)
|
|
212
|
+
expect(filterUsedByDataUrl(filter, datasetKey, visualizations, [])).toBe(true)
|
|
213
213
|
})
|
|
214
214
|
|
|
215
215
|
it('should return true when filter has usedBy array with visualization keys that match the datasetKey', () => {
|
|
216
216
|
const filter = { usedBy: ['viz1', 'viz3'], datasetKey: 'dataset1' }
|
|
217
217
|
const datasetKey = 'dataset1'
|
|
218
|
-
expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(true)
|
|
218
|
+
expect(filterUsedByDataUrl(filter, datasetKey, visualizations, [])).toBe(true)
|
|
219
219
|
})
|
|
220
220
|
|
|
221
221
|
it('should return false when filter has usedBy array with visualization keys that do not match the datasetKey', () => {
|
|
222
222
|
const filter = { usedBy: ['viz2'], datasetKey: 'dataset1' }
|
|
223
223
|
const datasetKey = 'dataset1'
|
|
224
|
-
expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(false)
|
|
224
|
+
expect(filterUsedByDataUrl(filter, datasetKey, visualizations, [])).toBe(false)
|
|
225
225
|
})
|
|
226
226
|
|
|
227
227
|
it('should return true when filter has usedBy array with a mix of matching and non-matching visualization keys', () => {
|
|
228
228
|
const filter = { usedBy: ['viz1', 'viz2'], datasetKey: 'dataset1' }
|
|
229
229
|
const datasetKey = 'dataset1'
|
|
230
|
-
expect(filterUsedByDataUrl(filter, datasetKey, visualizations)).toBe(true)
|
|
230
|
+
expect(filterUsedByDataUrl(filter, datasetKey, visualizations, [])).toBe(true)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should return true when used by a row', () => {
|
|
234
|
+
const filter = { usedBy: ['viz1', 'viz2', 2], datasetKey: 'dataset1' }
|
|
235
|
+
const datasetKey = 'dataset1'
|
|
236
|
+
expect(filterUsedByDataUrl(filter, datasetKey, visualizations, [{}, {}, { dataKey: 'dataset1' }])).toBe(true)
|
|
231
237
|
})
|
|
232
238
|
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { shouldLoadAllFilters } from '../shouldLoadAllFilters'
|
|
2
|
+
|
|
3
|
+
describe('shouldLoadAllFilters', () => {
|
|
4
|
+
it('returns false if not autoloading', () => {
|
|
5
|
+
delete window.location
|
|
6
|
+
window.location = new URL('https://www.example.com')
|
|
7
|
+
expect(shouldLoadAllFilters({ rows: [], visualizations: {}, dashboard: {} })).toBe(false)
|
|
8
|
+
})
|
|
9
|
+
it('returns true if missing data', () => {
|
|
10
|
+
delete window.location
|
|
11
|
+
window.location = new URL('https://www.example.com?cove-auto-load=true')
|
|
12
|
+
const config = {
|
|
13
|
+
dashboard: {},
|
|
14
|
+
visualizations: {
|
|
15
|
+
abc: {
|
|
16
|
+
dataKey: 'abcd'
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
rows: [],
|
|
20
|
+
datasets: {
|
|
21
|
+
abcd: {
|
|
22
|
+
data: []
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
expect(shouldLoadAllFilters(config)).toBe(true)
|
|
27
|
+
const config2 = {
|
|
28
|
+
multiDashboards: [
|
|
29
|
+
{
|
|
30
|
+
dashboard: {},
|
|
31
|
+
visualizations: {
|
|
32
|
+
abc: {
|
|
33
|
+
dataKey: 'abcd'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
rows: [],
|
|
37
|
+
datasets: {
|
|
38
|
+
abcd: {
|
|
39
|
+
data: []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
datasets: {
|
|
45
|
+
abcd: {
|
|
46
|
+
data: []
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
activeDashboard: 0
|
|
50
|
+
}
|
|
51
|
+
expect(shouldLoadAllFilters(config2)).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
it('returns false if no missing data', () => {
|
|
54
|
+
delete window.location
|
|
55
|
+
window.location = new URL('https://www.example.com?cove-auto-load=true')
|
|
56
|
+
const config = {
|
|
57
|
+
dashboard: {},
|
|
58
|
+
visualizations: {
|
|
59
|
+
abc: {
|
|
60
|
+
dataKey: 'abcd'
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
rows: [],
|
|
64
|
+
datasets: {
|
|
65
|
+
abcd: {
|
|
66
|
+
data: [{}]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
expect(shouldLoadAllFilters(config)).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
it('returns true when theres an autoloading filter and no apply filters', () => {
|
|
73
|
+
delete window.location
|
|
74
|
+
window.location = new URL('https://www.example.com')
|
|
75
|
+
const config = {
|
|
76
|
+
dashboard: {},
|
|
77
|
+
visualizations: {
|
|
78
|
+
abc: {
|
|
79
|
+
visualizationType: 'dashboardFilters',
|
|
80
|
+
dataKey: 'abcd',
|
|
81
|
+
autoLoad: true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
rows: [],
|
|
85
|
+
datasets: {
|
|
86
|
+
abcd: { data: [] }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
expect(shouldLoadAllFilters(config)).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('returns false when theres an autoloading filter and apply filters', () => {
|
|
93
|
+
delete window.location
|
|
94
|
+
window.location = new URL('https://www.example.com')
|
|
95
|
+
const config = {
|
|
96
|
+
dashboard: {},
|
|
97
|
+
visualizations: {
|
|
98
|
+
abc: {
|
|
99
|
+
visualizationType: 'dashboardFilters',
|
|
100
|
+
dataKey: 'abcd',
|
|
101
|
+
autoLoad: true
|
|
102
|
+
},
|
|
103
|
+
abc2: {
|
|
104
|
+
visualizationType: 'dashboardFilters',
|
|
105
|
+
dataKey: 'abcde',
|
|
106
|
+
filterBehavior: 'Apply Button'
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
rows: [],
|
|
110
|
+
datasets: {
|
|
111
|
+
abcd: { data: [] },
|
|
112
|
+
abcde: { data: [] }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
expect(shouldLoadAllFilters(config)).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
})
|
package/src/scss/grid.scss
CHANGED
|
@@ -51,27 +51,25 @@ $red: #f74242;
|
|
|
51
51
|
margin-top: 2em;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
.row-menu__btn:hover .row-menu__flyout {
|
|
55
|
-
transition: width 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
56
|
-
width: 180px;
|
|
57
|
-
|
|
58
|
-
li {
|
|
59
|
-
display: flex;
|
|
60
|
-
}
|
|
61
54
|
|
|
62
|
-
li + li {
|
|
63
|
-
margin-left: 0.3em;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
55
|
|
|
67
56
|
.row-menu__flyout {
|
|
57
|
+
background-color: var(--blue);
|
|
58
|
+
$blue: #005eaa;
|
|
59
|
+
background-color: #c2c2c2;
|
|
60
|
+
border-radius: 0.2em 0.2em 0 0;
|
|
61
|
+
outline: none;
|
|
62
|
+
|
|
63
|
+
padding: 0.2em 0.3em;
|
|
64
|
+
fill: #fff;
|
|
68
65
|
list-style: none;
|
|
69
66
|
display: flex;
|
|
70
67
|
justify-content: flex-start;
|
|
71
68
|
overflow: hidden;
|
|
69
|
+
transition: background-color 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
72
70
|
transition: width 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
73
71
|
z-index: 1;
|
|
74
|
-
width:
|
|
72
|
+
width: 35px;
|
|
75
73
|
|
|
76
74
|
li:not(.current) {
|
|
77
75
|
display: none;
|
|
@@ -89,6 +87,18 @@ $red: #f74242;
|
|
|
89
87
|
.row-menu__list--item {
|
|
90
88
|
display: flex;
|
|
91
89
|
}
|
|
90
|
+
&:hover {
|
|
91
|
+
transition: width 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
92
|
+
width: 180px;
|
|
93
|
+
background-color: lighten($blue, 8%);
|
|
94
|
+
li {
|
|
95
|
+
display: flex;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
li + li {
|
|
99
|
+
margin-left: 0.3em;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
.row-menu__btn {
|
|
@@ -96,7 +106,6 @@ $red: #f74242;
|
|
|
96
106
|
border-radius: 0.2em 0.2em 0 0;
|
|
97
107
|
outline: none;
|
|
98
108
|
transition: background-color 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
99
|
-
cursor: pointer;
|
|
100
109
|
padding: 0.2em 0.3em;
|
|
101
110
|
display: flex;
|
|
102
111
|
fill: #fff;
|
|
@@ -294,11 +303,6 @@ $red: #f74242;
|
|
|
294
303
|
}
|
|
295
304
|
}
|
|
296
305
|
|
|
297
|
-
.btn.add-row {
|
|
298
|
-
font-size: 1.1em;
|
|
299
|
-
width: 100%;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
306
|
.btn--fluid {
|
|
303
307
|
@extend .btn;
|
|
304
308
|
width: 100%;
|
|
@@ -359,11 +363,6 @@ $red: #f74242;
|
|
|
359
363
|
|
|
360
364
|
&:hover {
|
|
361
365
|
.row-menu .row-menu__btn {
|
|
362
|
-
background-color: var(--blue);
|
|
363
|
-
$blue: #005eaa;
|
|
364
|
-
&:hover {
|
|
365
|
-
background-color: lighten($blue, 8%);
|
|
366
|
-
}
|
|
367
366
|
|
|
368
367
|
&.row-menu__btn--edit {
|
|
369
368
|
background-color: transparent;
|
package/src/scss/main.scss
CHANGED
|
@@ -138,21 +138,6 @@
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
.btn {
|
|
141
|
-
background: #005eaa;
|
|
142
|
-
color: #fff;
|
|
143
|
-
border: 0;
|
|
144
|
-
padding: 0.4em 0.8em;
|
|
145
|
-
font-size: 0.9em;
|
|
146
|
-
display: block;
|
|
147
|
-
border-radius: 5px;
|
|
148
|
-
transition: 0.1s all;
|
|
149
|
-
cursor: pointer;
|
|
150
|
-
|
|
151
|
-
&[disabled] {
|
|
152
|
-
opacity: 0.5;
|
|
153
|
-
z-index: -1;
|
|
154
|
-
position: relative;
|
|
155
|
-
}
|
|
156
141
|
|
|
157
142
|
// Expand and Collapse Buttons for Multiviz Dashboard
|
|
158
143
|
&.expand-collapse-buttons {
|
|
@@ -260,15 +245,6 @@
|
|
|
260
245
|
width: 100%;
|
|
261
246
|
}
|
|
262
247
|
|
|
263
|
-
.cove-dashboard-filters-container {
|
|
264
|
-
z-index: 5;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
.cove-dashboard-filters {
|
|
268
|
-
display: inline-flex;
|
|
269
|
-
margin: 1em;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
248
|
@include breakpointClass(md) {
|
|
273
249
|
.dashboard-row {
|
|
274
250
|
flex-direction: row;
|
|
@@ -301,9 +277,6 @@
|
|
|
301
277
|
}
|
|
302
278
|
}
|
|
303
279
|
|
|
304
|
-
.dashboard-filters-section {
|
|
305
|
-
margin: 0 0 1em;
|
|
306
|
-
}
|
|
307
280
|
.builder-grid .editor-heading {
|
|
308
281
|
position: relative;
|
|
309
282
|
right: -2em;
|
|
@@ -105,8 +105,14 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
|
|
|
105
105
|
_.remove(newMultiDashboards, (_, index) => {
|
|
106
106
|
return index === action.payload
|
|
107
107
|
})
|
|
108
|
+
const config = {
|
|
109
|
+
...state.config,
|
|
110
|
+
multiDashboards: newMultiDashboards,
|
|
111
|
+
...newMultiDashboards[0],
|
|
112
|
+
activeDashboard: 0
|
|
113
|
+
}
|
|
108
114
|
if (newMultiDashboards.length === 0) return { ...state, config: _.omit(state.config, 'multiDashboards') }
|
|
109
|
-
return applyMultiDashboards(state, newMultiDashboards)
|
|
115
|
+
return applyMultiDashboards({ ...state, config }, newMultiDashboards)
|
|
110
116
|
}
|
|
111
117
|
case 'RENAME_DASHBOARD_TAB': {
|
|
112
118
|
const newMultiDashboards = state.config.multiDashboards.map(dashboard => {
|
|
@@ -144,7 +150,8 @@ const reducer = (state: DashboardState, action: DashboardActions): DashboardStat
|
|
|
144
150
|
case 'SWITCH_CONFIG': {
|
|
145
151
|
const slot = action.payload
|
|
146
152
|
const newConfigFields = state.config.multiDashboards[slot]
|
|
147
|
-
|
|
153
|
+
const _newDatasets = _.cloneDeep(state.data)
|
|
154
|
+
return { ...state, data: _newDatasets, config: { ...state.config, ...newConfigFields, activeDashboard: slot } }
|
|
148
155
|
}
|
|
149
156
|
case 'TOGGLE_ROW': {
|
|
150
157
|
const { rowIndex, colIndex } = action.payload
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Action } from '@cdc/core/types/Action'
|
|
2
|
+
|
|
3
|
+
type ADD_ERROR_MESSAGE = Action<'ADD_ERROR_MESSAGE', string>
|
|
4
|
+
type DISMISS_ERROR_MESSAGE = Action<'DISMISS_ERROR_MESSAGE', number>
|
|
5
|
+
|
|
6
|
+
type errorMessagesActions = ADD_ERROR_MESSAGE | DISMISS_ERROR_MESSAGE
|
|
7
|
+
export default errorMessagesActions
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import errorMessagesActions from './errorMessage.actions'
|
|
3
|
+
import { devToolsWrapper } from '@cdc/core/helpers/withDevTools'
|
|
4
|
+
|
|
5
|
+
export type errorMessagesState = string[]
|
|
6
|
+
|
|
7
|
+
const reducer = (state: errorMessagesState, action: errorMessagesActions): errorMessagesState => {
|
|
8
|
+
switch (action.type) {
|
|
9
|
+
case 'ADD_ERROR_MESSAGE': {
|
|
10
|
+
const message = action.payload
|
|
11
|
+
return [...state, message]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
case 'DISMISS_ERROR_MESSAGE': {
|
|
15
|
+
const messages = [...state]
|
|
16
|
+
_.remove(messages, (_, index) => {
|
|
17
|
+
return index === action.payload
|
|
18
|
+
})
|
|
19
|
+
return messages
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default devToolsWrapper<errorMessagesState, errorMessagesActions>(reducer)
|