@cdc/dashboard 4.24.5 → 4.24.9-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/cdcdashboard.js +147572 -128223
  2. package/examples/custom/css/respiratory.css +236 -0
  3. package/examples/custom/js/respiratory.js +242 -0
  4. package/examples/default-multi-dataset-shared-filter.json +1729 -0
  5. package/examples/ed-visits-county-file.json +618 -0
  6. package/examples/filtered-dash.json +6 -21
  7. package/examples/single-state-dashboard-filters.json +421 -0
  8. package/examples/state-level.json +90136 -0
  9. package/examples/state-points.json +10474 -0
  10. package/examples/test-file.json +147 -0
  11. package/examples/testing.json +94456 -0
  12. package/index.html +25 -4
  13. package/package.json +12 -11
  14. package/src/CdcDashboard.tsx +5 -1
  15. package/src/CdcDashboardComponent.tsx +250 -327
  16. package/src/DashboardContext.tsx +15 -1
  17. package/src/_stories/Dashboard.stories.tsx +158 -40
  18. package/src/_stories/_mock/api-filter-chart.json +11 -35
  19. package/src/_stories/_mock/api-filter-map.json +17 -31
  20. package/src/_stories/_mock/bump-chart.json +3554 -0
  21. package/src/_stories/_mock/methodology.json +412 -0
  22. package/src/_stories/_mock/methodologyAPI.ts +90 -0
  23. package/src/_stories/_mock/multi-viz.json +3 -4
  24. package/src/_stories/_mock/pivot-filter.json +14 -12
  25. package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
  26. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  27. package/src/components/Column.tsx +1 -1
  28. package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
  29. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  30. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  31. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
  32. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  33. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
  34. package/src/components/DashboardFilters/index.ts +3 -0
  35. package/src/components/DataDesignerModal.tsx +9 -9
  36. package/src/components/ExpandCollapseButtons.tsx +20 -0
  37. package/src/components/Header/Header.tsx +1 -102
  38. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
  39. package/src/components/Row.tsx +52 -19
  40. package/src/components/Toggle/Toggle.tsx +2 -4
  41. package/src/components/VisualizationRow.tsx +169 -30
  42. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  43. package/src/components/VisualizationsPanel/index.ts +1 -0
  44. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  45. package/src/components/Widget.tsx +27 -90
  46. package/src/helpers/FilterBehavior.ts +4 -0
  47. package/src/helpers/addValuesToDashboardFilters.ts +49 -0
  48. package/src/helpers/apiFilterHelpers.ts +103 -0
  49. package/src/helpers/changeFilterActive.ts +39 -0
  50. package/src/helpers/filterData.ts +10 -48
  51. package/src/helpers/generateValuesForFilter.ts +1 -1
  52. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  53. package/src/helpers/getFilteredData.ts +7 -5
  54. package/src/helpers/getVizConfig.ts +23 -2
  55. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  56. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  57. package/src/helpers/iconHash.tsx +5 -3
  58. package/src/helpers/loadAPIFilters.ts +74 -0
  59. package/src/helpers/mapDataToConfig.ts +29 -0
  60. package/src/helpers/processData.ts +2 -3
  61. package/src/helpers/reloadURLHelpers.ts +102 -0
  62. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
  63. package/src/helpers/tests/apiFilterHelpers.test.ts +155 -0
  64. package/src/helpers/tests/filterData.test.ts +1 -93
  65. package/src/helpers/tests/getFilteredData.test.ts +86 -0
  66. package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +220 -0
  67. package/src/helpers/tests/reloadURLHelpers.test.ts +232 -0
  68. package/src/scss/editor-panel.scss +1 -1
  69. package/src/scss/grid.scss +34 -27
  70. package/src/scss/main.scss +41 -3
  71. package/src/scss/variables.scss +4 -0
  72. package/src/store/dashboard.actions.ts +12 -4
  73. package/src/store/dashboard.reducer.ts +30 -4
  74. package/src/types/APIFilter.ts +1 -5
  75. package/src/types/ConfigRow.ts +2 -0
  76. package/src/types/Dashboard.ts +1 -1
  77. package/src/types/DashboardConfig.ts +2 -4
  78. package/src/types/DashboardFilters.ts +7 -0
  79. package/src/types/InitialState.ts +1 -1
  80. package/src/types/MultiDashboard.ts +2 -2
  81. package/src/types/SharedFilter.ts +4 -6
  82. package/src/types/Tab.ts +1 -1
  83. package/src/components/Filters.tsx +0 -88
  84. package/src/components/Header/FilterModal.tsx +0 -510
  85. package/src/components/VisualizationsPanel.tsx +0 -95
  86. package/src/helpers/getApiFilterKey.ts +0 -5
@@ -10,14 +10,14 @@ export const getApplicableFilters = (dashboard: Dashboard, key: string | number)
10
10
  return c?.length > 0 ? c : false
11
11
  }
12
12
 
13
- export const getFilteredData = (state: DashboardState, initialFilteredData = {}, dataOverride?: Object) => {
14
- const newFilteredData = initialFilteredData
13
+ export const getFilteredData = (state: DashboardState, initialFilteredData?: Record<string, any>, dataOverride?: Object) => {
14
+ const newFilteredData = initialFilteredData || {}
15
15
  const { config } = state
16
16
  getVizKeys(config).forEach(key => {
17
17
  const applicableFilters = getApplicableFilters(config.dashboard, key)
18
18
  if (applicableFilters) {
19
19
  const { dataKey, data, dataDescription } = config.visualizations[key]
20
- const _data = state.data[dataKey] || data
20
+ const _data = (dataOverride || state.data)[dataKey] || data
21
21
  const formattedData = dataOverride?.[dataKey] || (dataDescription ? getFormattedData(_data, dataDescription) : _data)
22
22
 
23
23
  newFilteredData[key] = filterData(applicableFilters, formattedData)
@@ -26,12 +26,14 @@ export const getFilteredData = (state: DashboardState, initialFilteredData = {},
26
26
  config.rows.forEach((row, index) => {
27
27
  if (row.dataKey) {
28
28
  const applicableFilters = getApplicableFilters(config.dashboard, index)
29
+ const { dataKey, data, dataDescription } = row
30
+ const _data = (dataOverride || state.data)[dataKey] || data
29
31
  if (applicableFilters) {
30
- const { dataKey, data, dataDescription } = row
31
- const _data = state.data[dataKey] || data
32
32
  const formattedData = dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
33
33
 
34
34
  newFilteredData[index] = filterData(applicableFilters, formattedData)
35
+ } else {
36
+ newFilteredData[index] = _data || []
35
37
  }
36
38
  }
37
39
  })
@@ -1,12 +1,33 @@
1
1
  import _ from 'lodash'
2
2
  import { MultiDashboardConfig } from '../types/MultiDashboard'
3
3
  import DataTransform from '@cdc/core/helpers/DataTransform'
4
+ import { getApplicableFilters } from './getFilteredData'
5
+ import { filterData } from './filterData'
6
+ import Footnotes from '@cdc/core/types/Footnotes'
4
7
 
5
8
  const transform = new DataTransform()
6
9
 
10
+ export const getFootnotesVizConfig = (vizKey: string, rowNumber: number, config: MultiDashboardConfig) => {
11
+ const visualizationConfig = _.cloneDeep(config.visualizations[vizKey])
12
+
13
+ const data = config.datasets[visualizationConfig.dataKey]?.data
14
+ const dataColumns = data?.length ? Object.keys(data[0]) : []
15
+ const filters = (getApplicableFilters(config.dashboard, rowNumber) || []).filter(filter => dataColumns.includes(filter.columnName))
16
+ if (filters.length) {
17
+ visualizationConfig.formattedData = filterData(filters, data)
18
+ }
19
+ visualizationConfig.data = data
20
+ return visualizationConfig as Footnotes
21
+ }
22
+
7
23
  export const getVizConfig = (visualizationKey: string, rowNumber: number, config: MultiDashboardConfig, data: Object, filteredData?: Object) => {
24
+ if (rowNumber === undefined) return {}
8
25
  const visualizationConfig = _.cloneDeep(config.visualizations[visualizationKey])
9
26
  const rowData = config.rows[rowNumber]
27
+ if (rowData.footnotesId && rowData.footnotesId === visualizationKey) {
28
+ // return the footnotes visualization config with filtered data
29
+ return getFootnotesVizConfig(visualizationKey, rowNumber, config)
30
+ }
10
31
  if (rowData?.dataKey) {
11
32
  // data configured on the row
12
33
  Object.assign(visualizationConfig, _.pick(rowData, ['dataKey', 'dataDescription', 'formattedData', 'data']))
@@ -16,13 +37,13 @@ export const getVizConfig = (visualizationKey: string, rowNumber: number, config
16
37
  const filteredVizData = filteredData?.[rowNumber] ?? filteredData?.[visualizationKey]
17
38
 
18
39
  if (filteredVizData) {
19
- visualizationConfig.data = filteredVizData
40
+ visualizationConfig.data = filteredVizData || []
20
41
  if (visualizationConfig.formattedData) {
21
42
  visualizationConfig.formattedData = visualizationConfig.data
22
43
  }
23
44
  } else {
24
45
  const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
25
- visualizationConfig.data = data[dataKey]
46
+ visualizationConfig.data = data[dataKey] || []
26
47
  if (visualizationConfig.formattedData) {
27
48
  visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
28
49
  }
@@ -1,9 +1,10 @@
1
1
  import { ConfigRow } from '../types/ConfigRow'
2
2
 
3
- export const getVizRowColumnLocator = (rows: ConfigRow[]) =>
3
+ export const getVizRowColumnLocator = (rows: ConfigRow[]): Record<string, { row: number; column: number }> =>
4
4
  rows.reduce((acc, curr, index) => {
5
5
  curr.columns?.forEach((column, columnIndex) => {
6
6
  if (column.widget !== undefined) acc[column.widget] = { row: index, column: columnIndex }
7
7
  })
8
+ if (curr.footnotesId) acc[curr.footnotesId] = { row: index, column: 0 }
8
9
  return acc
9
10
  }, {})
@@ -0,0 +1,5 @@
1
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
2
+
3
+ export const hasDashboardApplyBehavior = (visualizations: Record<string, AnyVisualization>) => {
4
+ return Object.values(visualizations).some(v => v.filterBehavior === 'Apply Button' && v.type === 'dashboardFilters')
5
+ }
@@ -1,10 +1,11 @@
1
1
  import Icon from '@cdc/core/components/ui/Icon'
2
- import { Visualization } from '@cdc/core/types/Visualization'
2
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
3
3
 
4
4
  export const iconHash = {
5
5
  'data-bite': <Icon display='databite' base />,
6
6
  Bar: <Icon display='chartBar' base />,
7
7
  'Spark Line': <Icon display='chartLine' />,
8
+ 'Bump Chart': <Icon display='chartLine' />,
8
9
  'waffle-chart': <Icon display='grid' base />,
9
10
  'markup-include': <Icon display='code' base />,
10
11
  Line: <Icon display='chartLine' base />,
@@ -14,14 +15,15 @@ export const iconHash = {
14
15
  world: <Icon display='mapWorld' base />,
15
16
  'single-state': <Icon display='mapAl' base />,
16
17
  gear: <Icon display='gear' base />,
18
+ gearMulti: <Icon display='gearMulti' base />,
17
19
  tools: <Icon display='tools' base />,
18
20
  'filtered-text': <Icon display='filtered-text' base />,
19
- 'filter-dropdowns': <Icon display='filter-dropdowns' base />,
21
+ dashboardFilters: <Icon display='dashboardFilters' base />,
20
22
  table: <Icon display='table' base />,
21
23
  Sankey: <Icon display='sankey' base />
22
24
  }
23
25
 
24
- export const getIcon = (visualization: Visualization) => {
26
+ export const getIcon = (visualization: AnyVisualization) => {
25
27
  const { type, visualizationType, general } = visualization
26
28
  if (visualizationType) return iconHash[visualizationType]
27
29
  if (general?.geoType) {
@@ -0,0 +1,74 @@
1
+ import _ from 'lodash'
2
+ import { APIFilterDropdowns } from '../components/DashboardFilters'
3
+ import { SharedFilter } from '../types/SharedFilter'
4
+ import * as apiFilterHelpers from './apiFilterHelpers'
5
+ import { APIFilter } from '../types/APIFilter'
6
+
7
+ export const loadAPIFiltersFactory = (
8
+ dispatch: Function,
9
+ setAPIFilterDropdowns: Function,
10
+ autoLoadFilterIndexes: number[]
11
+ ) => {
12
+ const loadAPIFilters = (
13
+ sharedFilters: SharedFilter[],
14
+ dropdowns: APIFilterDropdowns,
15
+ recursiveLimit = 3
16
+ ): Promise<SharedFilter[]> => {
17
+ if (!sharedFilters) return
18
+ sharedFilters = sharedFilters.map((filter, index) =>
19
+ apiFilterHelpers.setAutoLoadDefaultValue(
20
+ index,
21
+ dropdowns[filter.apiFilter?.apiEndpoint],
22
+ sharedFilters,
23
+ autoLoadFilterIndexes
24
+ )
25
+ )
26
+ const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
27
+ const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
28
+ const toFetch = apiFilterHelpers.getToFetch(sharedFilters, dropdowns)
29
+ const newDropdowns = _.cloneDeep(dropdowns)
30
+ return Promise.all(
31
+ Object.keys(toFetch).map(
32
+ endpoint =>
33
+ new Promise<void>(resolve => {
34
+ fetch(endpoint)
35
+ .then(resp => resp.json())
36
+ .then(data => {
37
+ if (!Array.isArray(data)) {
38
+ console.error('COVE only supports response data in the shape Array<Object>')
39
+ return
40
+ }
41
+ const [_key, index] = toFetch[endpoint]
42
+ const apiFilter = filterLookup.get(_key) as APIFilter
43
+ const _filterValues = apiFilterHelpers.getFilterValues(data, apiFilter)
44
+ newDropdowns[_key] = _filterValues
45
+ const newDefaultSelectedFilter = apiFilterHelpers.setAutoLoadDefaultValue(
46
+ index,
47
+ _filterValues,
48
+ sharedFilters,
49
+ autoLoadFilterIndexes
50
+ )
51
+ sharedFilters[index] = newDefaultSelectedFilter
52
+ })
53
+ .catch(console.error)
54
+ .finally(() => {
55
+ resolve()
56
+ })
57
+ })
58
+ )
59
+ ).then(() => {
60
+ const finishedLoading = sharedFilters.reduce((acc, curr, index) => {
61
+ if (autoLoadFilterIndexes.includes(index) && !curr.active) return false
62
+ return acc
63
+ }, true)
64
+ if (finishedLoading || recursiveLimit === 0) {
65
+ setAPIFilterDropdowns(newDropdowns)
66
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
67
+ return sharedFilters
68
+ } else {
69
+ return loadAPIFilters(sharedFilters, newDropdowns, recursiveLimit - 1)
70
+ }
71
+ })
72
+ }
73
+ return loadAPIFilters
74
+ }
@@ -0,0 +1,29 @@
1
+ import { getFormattedData } from './getFormattedData'
2
+ import { DashboardConfig } from '../types/DashboardConfig'
3
+
4
+ const mapDataToVisualizations = (config: DashboardConfig) => {
5
+ Object.keys(config.visualizations).forEach((vizKey, i) => {
6
+ const viz = config.visualizations[vizKey]
7
+ if (viz.dataKey && !viz.data) {
8
+ const data = config.datasets[viz.dataKey].data
9
+ config.visualizations[vizKey].data = data
10
+ config.visualizations[vizKey].formattedData = getFormattedData(data, viz.dataDescription)
11
+ }
12
+ })
13
+ }
14
+
15
+ const mapDataToRows = (config: DashboardConfig) => {
16
+ config.rows.forEach((row, i) => {
17
+ if (row.dataKey && !row.data) {
18
+ const data = config.datasets[row.dataKey].data
19
+ config.rows[i].data = data
20
+ config.rows[i].formattedData = getFormattedData(data, row.dataDescription)
21
+ }
22
+ })
23
+ }
24
+
25
+ export const mapDataToConfig = (config: DashboardConfig) => {
26
+ mapDataToVisualizations(config)
27
+ mapDataToRows(config)
28
+ return config
29
+ }
@@ -1,10 +1,9 @@
1
- import { FilterBehavior } from '../components/Header/Header'
2
1
  import { DataSet } from '../types/DataSet'
3
2
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
4
3
  import { getFormattedData } from './getFormattedData'
5
4
 
6
- export const processData = async (dataSet: DataSet, filterBehavior) => {
7
- if (dataSet.dataUrl && filterBehavior !== FilterBehavior.Apply) {
5
+ export const processData = async (dataSet: DataSet, hasFilterChangeBehavior: boolean) => {
6
+ if (dataSet.dataUrl && hasFilterChangeBehavior) {
8
7
  const dataset = await fetchRemoteData(`${dataSet.dataUrl}`)
9
8
  return getFormattedData(dataset, dataSet.dataDescription)
10
9
  }
@@ -0,0 +1,102 @@
1
+ import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
2
+ import { SharedFilter } from '../types/SharedFilter'
3
+ import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
4
+ import { AnyVisualization, Visualization } from '@cdc/core/types/Visualization'
5
+ import _ from 'lodash'
6
+
7
+ export const isUpdateNeeded = (
8
+ filters: SharedFilter[],
9
+ currentQueryParams: Record<string, string>,
10
+ newQueryParams: Record<string, string>
11
+ ): boolean => {
12
+ let needsUpdate = false
13
+ filters.find(filter => {
14
+ if (filter.type === 'urlfilter' && !Array.isArray(filter.active) && filter.filterBy === 'File Name') {
15
+ needsUpdate = true
16
+ return true
17
+ }
18
+ })
19
+ Object.keys(newQueryParams).forEach(updatedParam => {
20
+ if (decodeURIComponent(newQueryParams[updatedParam]) !== currentQueryParams[updatedParam]) {
21
+ needsUpdate = true
22
+ }
23
+ })
24
+ return needsUpdate
25
+ }
26
+
27
+ export const getDataURL = (updatedQSParams: Record<string, string | string[]>, dataUrl: URL, newFileName: string) => {
28
+ const _params = Object.keys(updatedQSParams).flatMap(key => {
29
+ const value = updatedQSParams[key]
30
+ if (value === undefined) return []
31
+ if (typeof value === 'string' && (value as String).match(/undefined/)) return []
32
+ if (Array.isArray(value)) return value.map(v => ({ key, value: v }))
33
+ return { key, value }
34
+ })
35
+ const baseURL = dataUrl.origin + dataUrl.pathname
36
+ let dataUrlFinal = `${baseURL}${gatherQueryParams(baseURL, _params)}`
37
+
38
+ if (newFileName !== '') {
39
+ const fileExtension = dataUrl.pathname.split('.').pop()
40
+ const pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
41
+ dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(
42
+ baseURL,
43
+ _params
44
+ )}`
45
+ }
46
+ return dataUrlFinal
47
+ }
48
+
49
+ export const getNewFileName = (newFileName: string, filter: SharedFilter, datasetKey: string) => {
50
+ const replacements = {
51
+ 'Remove Spaces': '',
52
+ 'Keep Spaces': ' ',
53
+ 'Replace With Underscore': '_'
54
+ }
55
+ let fileName = newFileName
56
+ if (filter.datasetKey === datasetKey) {
57
+ if (filter.fileName) {
58
+ // if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
59
+ fileName = capitalizeSplitAndJoin.call(
60
+ String(filter.fileName),
61
+ ' ',
62
+ replacements[filter.whitespaceReplacement ?? 'Keep Spaces']
63
+ )
64
+ } else {
65
+ // if no file name is entered use the default active filter. ie. /activeFilter.json
66
+ fileName = filter.active as string
67
+ }
68
+ }
69
+
70
+ if (fileName?.includes('${query}')) {
71
+ fileName = fileName.replace(
72
+ '${query}',
73
+ capitalizeSplitAndJoin.call(
74
+ String(filter.active),
75
+ ' ',
76
+ replacements[filter.whitespaceReplacement ?? 'Keep Spaces']
77
+ )
78
+ )
79
+ }
80
+
81
+ return fileName
82
+ }
83
+
84
+ export const getVisualizationsWithFormattedData = (visualizations: Record<string, Visualization>, newData: Object) => {
85
+ return Object.keys(visualizations).reduce((acc, vizKey) => {
86
+ const dataKey = visualizations[vizKey].dataKey
87
+ if (newData[dataKey]) {
88
+ acc[vizKey].formattedData = newData[dataKey]
89
+ }
90
+ return acc
91
+ }, _.cloneDeep(visualizations))
92
+ }
93
+
94
+ export const filterUsedByDataUrl = (
95
+ filter: SharedFilter,
96
+ datasetKey: string,
97
+ visualizations: Record<string, AnyVisualization>
98
+ ) => {
99
+ if (!filter.usedBy || !filter.usedBy.length) return true
100
+ const vizUsingFilters = filter.usedBy?.map(vizKey => visualizations[vizKey])
101
+ return vizUsingFilters?.some(viz => viz?.dataKey === datasetKey)
102
+ }
@@ -0,0 +1,44 @@
1
+ import { SharedFilter } from '../../types/SharedFilter'
2
+ import { addValuesToDashboardFilters } from '../addValuesToDashboardFilters'
3
+
4
+ describe('addValuesToDashboardFilters', () => {
5
+ const colA = { columnName: 'colA', id: 11, active: 'apple', values: [], type: 'datafilter' } as SharedFilter
6
+ const colB = { columnName: 'colB', id: 22, active: '1', values: [], type: 'datafilter' } as SharedFilter
7
+ const colC = { columnName: 'colC', id: 33, values: [], setByQueryParameter: 'colC', type: 'datafilter' } as SharedFilter
8
+
9
+ const data = {
10
+ key: [
11
+ { colA: 'apple', colB: 3, colC: 'abc' },
12
+ { colA: 'apple', colB: 1, colC: 'bcd' },
13
+ { colA: 'pear', colB: 4, colC: 'test' }
14
+ ]
15
+ }
16
+ const filters = [colA, colC, colB]
17
+ it('adds filter values', () => {
18
+ const newFilters = addValuesToDashboardFilters(filters, data)
19
+ expect(newFilters[0].values).toEqual(['apple', 'pear'])
20
+ })
21
+ it('converts to multiselect', () => {
22
+ colA.multiSelect = true
23
+ const newFilters = addValuesToDashboardFilters(filters, data)
24
+ expect(newFilters[0].active).toEqual(['apple'])
25
+ })
26
+
27
+ it('sets active value by query string', () => {
28
+ delete window.location
29
+ window.location = new URL('https://www.example.com?colC=test')
30
+ const newFilters = addValuesToDashboardFilters(filters, data)
31
+ expect(newFilters[1].active).toEqual('test')
32
+ })
33
+ const colA2 = { apiFilter: { valueSelector: 'colA' }, id: 11, active: 'apple', values: [], type: 'urlfilter' } as SharedFilter
34
+ const colB2 = { apiFilter: { valueSelector: 'colB' }, id: 22, active: '1', values: [], type: 'urlfilter' } as SharedFilter
35
+ const colC2 = { apiFilter: { valueSelector: 'colC' }, id: 33, values: [], setByQueryParameter: 'colC', type: 'urlfilter' } as SharedFilter
36
+ const filters2 = [colA2, colC2, colB2]
37
+ it('skips urlfilters', () => {
38
+ // urlfilter reloading happens in the dashboard in the loadAPIFilters function
39
+ delete window.location
40
+ window.location = new URL('https://www.example.com?colC=test')
41
+ const newFilters = addValuesToDashboardFilters(filters2, data)
42
+ expect(newFilters[1].active).toEqual(undefined)
43
+ })
44
+ })
@@ -0,0 +1,155 @@
1
+ import { setAutoLoadDefaultValue, getToFetch, getFilterValues, getLoadingFilterMemo } from '../apiFilterHelpers'
2
+ import _ from 'lodash'
3
+ import type { APIFilterDropdowns } from '../../components/DashboardFilters'
4
+
5
+ describe('getLoadingFilterMemo', () => {
6
+ it('should return correct APIFilterDropdowns for valid inputs', () => {
7
+ const sharedAPIFilters = ['endpoint1', 'endpoint2']
8
+ const apiFilterDropdowns: APIFilterDropdowns = {
9
+ endpoint1: { text: 'text1', value: 'value1' }
10
+ }
11
+ const expectedOutput: APIFilterDropdowns = {
12
+ endpoint1: { text: 'text1', value: 'value1' },
13
+ endpoint2: null
14
+ }
15
+ expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
16
+ })
17
+
18
+ it('should return an empty object for empty sharedAPIFilters', () => {
19
+ const sharedAPIFilters: string[] = []
20
+ const apiFilterDropdowns: APIFilterDropdowns = {
21
+ endpoint1: { text: 'text1', value: 'value1' }
22
+ }
23
+ const expectedOutput: APIFilterDropdowns = {}
24
+ expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
25
+ })
26
+
27
+ it('should return APIFilterDropdowns with null values for empty apiFilterDropdowns', () => {
28
+ const sharedAPIFilters = ['endpoint1', 'endpoint2']
29
+ const apiFilterDropdowns: APIFilterDropdowns = {}
30
+ const expectedOutput: APIFilterDropdowns = {
31
+ endpoint1: null,
32
+ endpoint2: null
33
+ }
34
+ expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
35
+ })
36
+
37
+ it('should not overwrite existing data in apiFilterDropdowns, so return original dropdowns', () => {
38
+ const sharedAPIFilters = ['endpoint1', 'endpoint2']
39
+ const apiFilterDropdowns: APIFilterDropdowns = {
40
+ endpoint1: { text: 'text1', value: 'value1' },
41
+ endpoint2: { text: 'text2', value: 'value2' }
42
+ }
43
+ expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(apiFilterDropdowns)
44
+ })
45
+ })
46
+
47
+ describe('getFilterValues', () => {
48
+ it('should return correct filter values for valid inputs', () => {
49
+ const data = [{ key1: 'value1', key2: 'value2' }]
50
+ const apiFilter = { textSelector: 'key1', valueSelector: 'key2' }
51
+ const expectedOutput = [{ text: 'value1', value: 'value2' }]
52
+ expect(getFilterValues(data, apiFilter)).toEqual(expectedOutput)
53
+ delete apiFilter.textSelector
54
+ const expectedOutput2 = [{ text: 'value2', value: 'value2' }]
55
+ expect(getFilterValues(data, apiFilter)).toEqual(expectedOutput2)
56
+ })
57
+ })
58
+
59
+ describe('getToFetch', () => {
60
+ it('should return an empty object when sharedAPIFilters is empty', () => {
61
+ const result = getToFetch([], {})
62
+ expect(result).toEqual({})
63
+ })
64
+
65
+ it('should return an object with endpoints when apiFilterDropdowns is empty', () => {
66
+ const sharedAPIFilters = [{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] }]
67
+ const result = getToFetch(sharedAPIFilters, {})
68
+ expect(result).toEqual({ '/endpoint1': ['/endpoint1', 0] })
69
+ })
70
+
71
+ it('should return and empty object when sharedAPIFilters contains filters with no parents', () => {
72
+ const sharedAPIFilters = [{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] }]
73
+ const apiFilterDropdowns = { '/endpoint1': true }
74
+ const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
75
+ expect(result).toEqual({})
76
+ })
77
+
78
+ it('should return an empty object when parentParams contains an empty value', () => {
79
+ const sharedAPIFilters = [
80
+ { key: 'parent1', apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] },
81
+ { apiFilter: { apiEndpoint: '/endpoint1' }, parents: ['parent1'] }
82
+ ]
83
+ const apiFilterDropdowns = { '/endpoint1': true }
84
+ const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
85
+ expect(result).toEqual({})
86
+ })
87
+
88
+ it('should return an empty object when parentParams contains an empty value', () => {
89
+ const sharedAPIFilters = [
90
+ { key: 'parent1', value: '', apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] },
91
+ { apiFilter: { apiEndpoint: '/endpoint1' }, parents: ['parent1'] }
92
+ ]
93
+ const apiFilterDropdowns = { '/endpoint1': true }
94
+ const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
95
+ expect(result).toEqual({})
96
+ })
97
+ })
98
+
99
+ describe('setAutoLoadDefaultValue', () => {
100
+ const dropdownOptions = [
101
+ { value: 'option1', label: 'Option 1' },
102
+ { value: 'option2', label: 'Option 2' }
103
+ ]
104
+
105
+ const sharedFilters = [
106
+ { key: 'filter1', active: null, queuedActive: null, parents: [] },
107
+ { key: 'filter2', active: null, queuedActive: null, parents: ['filter1'] }
108
+ ]
109
+
110
+ it('should return the original filter when autoLoadFilterIndexes is empty', () => {
111
+ const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFilters, [])
112
+ expect(result).toEqual(sharedFilters[0])
113
+ })
114
+
115
+ it('should return the original filter when dropdownOptions is empty', () => {
116
+ const result = setAutoLoadDefaultValue(0, [], sharedFilters, [0])
117
+ expect(result).toEqual(sharedFilters[0])
118
+ })
119
+
120
+ it('should return the original filter when dropdownOptions is undefined', () => {
121
+ const result = setAutoLoadDefaultValue(0, undefined, sharedFilters, [0])
122
+ expect(result).toEqual(sharedFilters[0])
123
+ })
124
+
125
+ it('should return the original filter when sharedFilterIndex is not in autoLoadFilterIndexes', () => {
126
+ const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFilters, [1])
127
+ expect(result).toEqual(sharedFilters[0])
128
+ })
129
+
130
+ it('should return the original filter when not all parent filters are selected', () => {
131
+ const result = setAutoLoadDefaultValue(1, dropdownOptions, sharedFilters, [1])
132
+ expect(result).toEqual(sharedFilters[1])
133
+ })
134
+
135
+ it('should assign the default value from dropdownOptions when no active value is set', () => {
136
+ const sharedFiltersCopy = _.cloneDeep(sharedFilters)
137
+ sharedFiltersCopy[0].active = null
138
+ const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
139
+ expect(result.active).toEqual('option1')
140
+ })
141
+
142
+ it('should retain the current active value if it exists in dropdownOptions', () => {
143
+ const sharedFiltersCopy = _.cloneDeep(sharedFilters)
144
+ sharedFiltersCopy[0].active = 'option1'
145
+ const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
146
+ expect(result.active).toEqual('option1')
147
+ })
148
+
149
+ it('should assign the default value if the current active value does not exist in dropdownOptions', () => {
150
+ const sharedFiltersCopy = _.cloneDeep(sharedFilters)
151
+ sharedFiltersCopy[0].active = 'nonexistent'
152
+ const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
153
+ expect(result.active).toEqual('option1')
154
+ })
155
+ })
@@ -1,3 +1,4 @@
1
+ import _ from 'lodash'
1
2
  import { SharedFilter } from '../../types/SharedFilter'
2
3
  import { filterData } from '../filterData'
3
4
 
@@ -36,48 +37,6 @@ describe('filterData', () => {
36
37
  expect(result).toEqual([{ name: 'John', age: 30 }])
37
38
  })
38
39
 
39
- it('causes sideEffects to filters', () => {
40
- // the side effect is not desired, but current functionality depends on the sideEffect.
41
- // hopefully the side effect will be refactored in the future to be a returned value.
42
- const filters = [
43
- { columnName: 'name', active: 'John', queuedActive: 'John', fileName: 'abc', key: 'name' },
44
- { columnName: 'age', fileName: 'abc', key: 'age' },
45
- { columnName: 'color', fileName: 'abc', key: 'color', parents: ['age'] }
46
- ] as SharedFilter[]
47
- const data = [
48
- { name: 'John', age: 30, color: 'blue' },
49
- { name: 'Jane', age: 25, color: 'red' },
50
- { name: 'John', age: 35, color: 'yellow' },
51
- { name: 'Jane', age: 30, color: 'green' }
52
- ]
53
-
54
- const result = filterData(filters, data)
55
-
56
- expect(result).toEqual([{ name: 'John', age: 30, color: 'blue' }])
57
-
58
- const sideEffectOfFiltering = [
59
- {
60
- columnName: 'name',
61
- active: 'John',
62
- queuedActive: 'John',
63
- fileName: 'abc',
64
- key: 'name',
65
- tier: 1
66
- },
67
- { columnName: 'age', fileName: 'abc', key: 'age', tier: 1 },
68
- {
69
- columnName: 'color',
70
- fileName: 'abc',
71
- key: 'color',
72
- parents: ['age'],
73
- tier: 2,
74
- values: ['blue', 'yellow'],
75
- active: 'blue'
76
- }
77
- ]
78
- expect(filters).toEqual(sideEffectOfFiltering)
79
- })
80
-
81
40
  it('should not include data that does not meet the filter criteria', () => {
82
41
  const filters = [
83
42
  //{ columnName: 'apple', fileName: 'abc', key: 'banana' },
@@ -95,55 +54,4 @@ describe('filterData', () => {
95
54
  const result = filterData(filters, data)
96
55
  expect(result).toEqual([{ name: 'John', age: 25, color: 'red' }])
97
56
  })
98
-
99
- it('should pivot data based on the provided filters', () => {
100
- const filters = [{ key: 'Race', type: 'datafilter', showDropdown: true, columnName: 'Race', pivot: 'Age-adjusted rate', usedBy: ['table1707935263149'] }] as SharedFilter[]
101
- const data = [
102
- {
103
- Race: 'Hispanic or Latino',
104
- 'Age-adjusted rate': '644.2',
105
- Year: '2016'
106
- },
107
- {
108
- Race: 'Non-Hispanic American Indian',
109
- 'Age-adjusted rate': '636.1',
110
- Year: '2016'
111
- },
112
- {
113
- Race: 'Non-Hispanic Black',
114
- 'Age-adjusted rate': '563.7',
115
- Year: '2016'
116
- },
117
- {
118
- Race: 'Hispanic or Latino',
119
- 'Age-adjusted rate': '644.2',
120
- Year: '2017'
121
- },
122
- {
123
- Race: 'Non-Hispanic American Indian',
124
- 'Age-adjusted rate': '636.1',
125
- Year: '2017'
126
- },
127
- {
128
- Race: 'Non-Hispanic Black',
129
- 'Age-adjusted rate': '563.7',
130
- Year: '2017'
131
- }
132
- ]
133
-
134
- expect(filterData(filters, data)).toEqual([
135
- {
136
- 'Hispanic or Latino': '644.2',
137
- 'Non-Hispanic American Indian': '636.1',
138
- 'Non-Hispanic Black': '563.7',
139
- Year: '2016'
140
- },
141
- {
142
- 'Hispanic or Latino': '644.2',
143
- 'Non-Hispanic American Indian': '636.1',
144
- 'Non-Hispanic Black': '563.7',
145
- Year: '2017'
146
- }
147
- ])
148
- })
149
57
  })