@cdc/dashboard 4.25.8 → 4.25.11

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 (88) hide show
  1. package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
  2. package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
  3. package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
  4. package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
  5. package/dist/cdcdashboard.js +80040 -75976
  6. package/examples/api-test/categories.json +18 -0
  7. package/examples/api-test/chart-data.json +602 -0
  8. package/examples/api-test/topics.json +47 -0
  9. package/examples/api-test/years.json +22 -0
  10. package/examples/markup-axis-label.json +4167 -0
  11. package/examples/private/DEV-10538.json +407 -0
  12. package/examples/private/DEV-11405.json +39112 -0
  13. package/examples/private/big-dashboard.json +39112 -0
  14. package/examples/private/brfs-2.json +1532 -0
  15. package/examples/private/brfs.json +2128 -2138
  16. package/examples/private/clade-2.json +430 -0
  17. package/examples/private/delete.json +32919 -0
  18. package/examples/private/diabetes.json +5582 -0
  19. package/examples/private/example-2.json +49796 -0
  20. package/examples/private/group-legend-test.json +328 -0
  21. package/examples/private/map.json +1211 -0
  22. package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +14041 -0
  23. package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +14041 -0
  24. package/examples/private/markup-footer/burden_toolkit_mortality_qaly_data.csv +18721 -0
  25. package/examples/private/markup-footer/burden_toolkit_mortality_yll_data.csv +18721 -0
  26. package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
  27. package/examples/private/markup-variables.json +1451 -0
  28. package/examples/private/markup.json +5471 -0
  29. package/examples/private/mpox.json +38128 -0
  30. package/examples/private/north-dakota.json +1132 -0
  31. package/examples/private/ophdst.json +38754 -0
  32. package/examples/private/pedro.json +1 -0
  33. package/examples/private/pivot.json +683 -0
  34. package/examples/private/reset.json +32920 -0
  35. package/examples/private/sewershed.json +435 -0
  36. package/examples/private/tobacco.json +1938 -0
  37. package/examples/test-api-filter-reset.json +132 -0
  38. package/index.html +2 -2
  39. package/package.json +16 -10
  40. package/src/CdcDashboard.tsx +1 -3
  41. package/src/CdcDashboardComponent.tsx +34 -16
  42. package/src/DashboardContext.tsx +5 -1
  43. package/src/_stories/Dashboard.API.stories.tsx +62 -0
  44. package/src/_stories/Dashboard.stories.tsx +492 -472
  45. package/src/_stories/_mock/api/cessation.json +1 -0
  46. package/src/_stories/_mock/api/data-explorer.json +1 -0
  47. package/src/_stories/_mock/api/explore-by-location.json +1 -0
  48. package/src/_stories/_mock/api/explore-by-topic.json +1 -0
  49. package/src/_stories/_mock/api/legislation.json +1 -0
  50. package/src/_stories/_mock/api/oral-health-data.json +1 -0
  51. package/src/_stories/_mock/custom-order-new-values.json +116 -0
  52. package/src/components/CollapsibleVisualizationRow.tsx +1 -1
  53. package/src/components/DashboardFilters/DashboardFilters.tsx +34 -23
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +29 -12
  55. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +81 -112
  56. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +82 -52
  57. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +130 -31
  58. package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +80 -21
  59. package/src/components/DataDesignerModal.tsx +227 -210
  60. package/src/components/Header/Header.tsx +13 -12
  61. package/src/components/Toggle/Toggle.tsx +48 -47
  62. package/src/components/VisualizationRow.tsx +13 -6
  63. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -0
  64. package/src/components/Widget/Widget.tsx +47 -18
  65. package/src/helpers/addValuesToDashboardFilters.ts +111 -60
  66. package/src/helpers/apiFilterHelpers.ts +190 -166
  67. package/src/helpers/filterData.ts +52 -7
  68. package/src/helpers/filterResetHelpers.ts +102 -0
  69. package/src/helpers/formatConfigBeforeSave.ts +137 -0
  70. package/src/helpers/getVizConfig.ts +36 -18
  71. package/src/helpers/loadAPIFilters.ts +109 -99
  72. package/src/helpers/reloadURLHelpers.ts +1 -1
  73. package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
  74. package/src/helpers/tests/formatConfigBeforeSave.test.ts +69 -0
  75. package/src/index.tsx +1 -1
  76. package/src/scss/editor-panel.scss +3 -431
  77. package/src/scss/grid.scss +7 -5
  78. package/src/scss/main.scss +1 -24
  79. package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
  80. package/src/types/DashboardFilters.ts +9 -8
  81. package/src/types/InitialState.ts +12 -12
  82. package/vite.config.js +1 -1
  83. package/vitest.config.ts +16 -0
  84. package/src/coreStyles_dashboard.scss +0 -3
  85. package/src/helpers/getAutoLoadVisualization.ts +0 -11
  86. package/src/scss/mixins.scss +0 -47
  87. package/src/scss/variables.scss +0 -5
  88. /package/dist/{cdcdashboard-548642e6.es.js → cdcdashboard-Ct2SB0vL.es.js} +0 -0
@@ -1,166 +1,190 @@
1
- import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
2
- import { APIFilterDropdowns, DropdownOptions } from '../components/DashboardFilters'
3
- import { APIFilter } from '../types/APIFilter'
4
- import { SharedFilter } from '../types/SharedFilter'
5
- import _ from 'lodash'
6
- import { getQueryParam } from '@cdc/core/helpers/queryStringUtils'
7
- import { FILTER_STYLE } from '../types/FilterStyles'
8
-
9
- /** key for the dropdowns object */
10
- type DropdownsKey = string
11
-
12
- export const getLoadingFilterMemo = (
13
- apiFiltersEndpoints: string[],
14
- apiFilterDropdowns,
15
- changedChildFilterIndexes = []
16
- ): APIFilterDropdowns =>
17
- apiFiltersEndpoints.reduce((acc, endpoint, currIndex) => {
18
- const _key: DropdownsKey = endpoint
19
- const hasChanged = changedChildFilterIndexes.includes(currIndex)
20
- if (apiFilterDropdowns[_key] && !hasChanged) {
21
- acc[_key] = apiFilterDropdowns[_key]
22
- } else {
23
- acc[_key] = undefined
24
- }
25
- return acc
26
- }, {})
27
-
28
- export const getParentParams = (
29
- childFilter: SharedFilter,
30
- sharedFilters: SharedFilter[]
31
- ): Record<'key' | 'value', string>[] | null => {
32
- const _parents = sharedFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
33
- if (!(_parents || []).length) return null
34
-
35
- return _parents.flatMap(filter => {
36
- if (filter.filterStyle === FILTER_STYLE.nestedDropdown) {
37
- const key = filter.apiFilter.valueSelector || ''
38
- const subKey = filter.apiFilter.subgroupValueSelector || ''
39
- const val = filter.queuedActive ? filter.queuedActive[0] : (filter.active as string) || ''
40
- const subVal = filter.queuedActive ? filter.queuedActive[1] : filter.subGrouping?.active || ''
41
- return [
42
- { key, value: val },
43
- { key: subKey, value: subVal }
44
- ]
45
- } else {
46
- const key = filter.queryParameter || filter.apiFilter.valueSelector || ''
47
- const value = filter.queuedActive || filter.active || ''
48
- if (Array.isArray(value)) {
49
- return value.map(_value => ({ key, value: _value.toString() }))
50
- }
51
- return [{ key, value: value.toString() }]
52
- }
53
- })
54
- }
55
-
56
- export const notAllParentsSelected = parentParams => parentParams?.some(({ value }) => value === '')
57
-
58
- export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
59
- const { textSelector, valueSelector, subgroupTextSelector, subgroupValueSelector } = apiFilter
60
- if (subgroupValueSelector) {
61
- const memo = {}
62
- data.forEach(v => {
63
- if (!memo[v[valueSelector]]) {
64
- memo[v[valueSelector]] = { text: v[textSelector || valueSelector], value: v[valueSelector], subOptions: [] }
65
- }
66
- memo[v[valueSelector]].subOptions.push({ text: v[subgroupTextSelector], value: v[subgroupValueSelector] })
67
- })
68
- return Object.values(memo)
69
- } else {
70
- }
71
- return data.map(v => ({ text: v[textSelector || valueSelector], value: v[valueSelector] }))
72
- }
73
-
74
- /** API endpoint to fetch */
75
- type Endpoint = string
76
- type SharedFilterIndex = number
77
- export const getToFetch = (
78
- sharedFilters: SharedFilter[],
79
- apiFilterDropdowns: APIFilterDropdowns
80
- ): Record<Endpoint, [DropdownsKey, SharedFilterIndex]> => {
81
- const toFetch = {}
82
- sharedFilters.forEach((filter, index) => {
83
- const baseEndpoint = filter.apiFilter?.apiEndpoint
84
- if (!baseEndpoint) return
85
- const _key = baseEndpoint
86
- if (apiFilterDropdowns[_key]) return // don't reload cached filter
87
- const parentParams = getParentParams(filter, sharedFilters)
88
-
89
- if (notAllParentsSelected(parentParams)) return // don't send request for dependent children filter options
90
-
91
- const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
92
- toFetch[endpoint] = [_key, index]
93
- })
94
- return toFetch
95
- }
96
-
97
- export const setActiveNestedDropdown = (dropdownOptions, sharedFilter) => {
98
- const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
99
- const defaultValue = dropdownOptions[0]?.value
100
- const subDefaultValue = dropdownOptions[0]?.subOptions[0].value
101
- const subDefaultQueryParamValue = getQueryParam(sharedFilter?.subGrouping.setByQueryParameter)
102
- if (!sharedFilter.active) {
103
- sharedFilter.active = defaultQueryParamValue || defaultValue
104
- sharedFilter.subGrouping.active = subDefaultQueryParamValue || subDefaultValue
105
- } else {
106
- const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
107
- sharedFilter.active = currentOption ? currentOption.value : defaultValue
108
- if (currentOption) {
109
- const currentSubOption = currentOption.subOptions.find(option => option.value === sharedFilter.subGrouping.active)
110
- sharedFilter.subGrouping.active = currentSubOption?.value || subDefaultValue
111
- } else {
112
- sharedFilter.subGrouping.active = subDefaultValue
113
- }
114
- }
115
- }
116
-
117
- export const setActiveMultiDropdown = (dropdownOptions, sharedFilter) => {
118
- const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
119
- const multiDefaultQueryParamValue = Array.isArray(defaultQueryParamValue)
120
- ? defaultQueryParamValue
121
- : defaultQueryParamValue?.split(',')
122
- const multiDefaultValue = defaultQueryParamValue ? multiDefaultQueryParamValue : [dropdownOptions[0]?.value]
123
- const currentOption = ((sharedFilter.active as string[]) || []).filter(activeVal =>
124
- dropdownOptions.find(option => option.value === activeVal)
125
- )
126
- sharedFilter.active = currentOption.length ? currentOption : multiDefaultValue
127
- }
128
-
129
- export const setAutoLoadDefaultValue = (
130
- sharedFilterIndex: number,
131
- dropdownOptions: DropdownOptions,
132
- sharedFilters: SharedFilter[],
133
- autoLoadFilterIndexes: number[]
134
- ): SharedFilter => {
135
- const sharedFiltersCopy = _.cloneDeep(sharedFilters)
136
- const sharedFilter = _.cloneDeep(sharedFiltersCopy[sharedFilterIndex])
137
- const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
138
- const hasQueryParameter = sharedFilter.setByQueryParameter ? defaultQueryParamValue !== undefined : false
139
- if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) {
140
- if (hasQueryParameter && sharedFilter.apiFilter) {
141
- const subQueryValue = getQueryParam(sharedFilter.subGrouping?.setByQueryParameter)
142
- const isNestedDropdown = subQueryValue !== undefined
143
- sharedFilter.queuedActive = isNestedDropdown ? [defaultQueryParamValue, subQueryValue] : defaultQueryParamValue
144
- }
145
- return sharedFilter // no autoLoading happening
146
- }
147
- if (autoLoadFilterIndexes.includes(sharedFilterIndex) || hasQueryParameter) {
148
- const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
149
- const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
150
- if (filterParents && notAllParentFiltersSelected) return sharedFilter
151
- if (sharedFilter.filterStyle === FILTER_STYLE.multiSelect) {
152
- setActiveMultiDropdown(dropdownOptions, sharedFilter)
153
- } else if (sharedFilter.filterStyle === FILTER_STYLE.nestedDropdown) {
154
- setActiveNestedDropdown(dropdownOptions, sharedFilter)
155
- } else {
156
- const defaultValue = dropdownOptions[0]?.value
157
- if (!sharedFilter.active) {
158
- sharedFilter.active = defaultQueryParamValue || defaultValue
159
- } else {
160
- const currentOption = dropdownOptions.find(option => option.value == sharedFilter.active) // loose equality required: 2017 should equal '2017'
161
- sharedFilter.active = currentOption ? currentOption.value : defaultValue
162
- }
163
- }
164
- }
165
- return sharedFilter
166
- }
1
+ import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
2
+ import { APIFilterDropdowns, DropdownOptions } from '../components/DashboardFilters'
3
+ import { APIFilter } from '../types/APIFilter'
4
+ import { SharedFilter } from '../types/SharedFilter'
5
+ import _ from 'lodash'
6
+ import { getQueryParam } from '@cdc/core/helpers/queryStringUtils'
7
+ import { FILTER_STYLE } from '../types/FilterStyles'
8
+
9
+ /** key for the dropdowns object */
10
+ type DropdownsKey = string
11
+
12
+ export const getLoadingFilterMemo = (
13
+ apiFiltersEndpoints: string[],
14
+ apiFilterDropdowns,
15
+ changedChildFilterIndexes = []
16
+ ): APIFilterDropdowns =>
17
+ apiFiltersEndpoints.reduce((acc, endpoint, currIndex) => {
18
+ const _key: DropdownsKey = endpoint
19
+ const hasChanged = changedChildFilterIndexes.includes(currIndex)
20
+ if (apiFilterDropdowns[_key] && !hasChanged) {
21
+ acc[_key] = apiFilterDropdowns[_key]
22
+ } else {
23
+ acc[_key] = undefined
24
+ }
25
+ return acc
26
+ }, {})
27
+
28
+ export const getParentParams = (
29
+ childFilter: SharedFilter,
30
+ sharedFilters: SharedFilter[]
31
+ ): Record<'key' | 'value', string>[] | null => {
32
+ const _parents = sharedFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
33
+ if (!(_parents || []).length) return null
34
+
35
+ return _parents.flatMap(filter => {
36
+ if (filter.filterStyle === FILTER_STYLE.nestedDropdown) {
37
+ const key = filter.apiFilter.valueSelector || ''
38
+ const subKey = filter.apiFilter.subgroupValueSelector || ''
39
+ const val = filter.queuedActive ? filter.queuedActive[0] : (filter.active as string) || ''
40
+ const subVal = filter.queuedActive ? filter.queuedActive[1] : filter.subGrouping?.active || ''
41
+ return [
42
+ { key, value: val },
43
+ { key: subKey, value: subVal }
44
+ ]
45
+ } else {
46
+ const key = filter.queryParameter || filter.apiFilter.valueSelector || ''
47
+ const value = filter.queuedActive || filter.active || ''
48
+ if (Array.isArray(value)) {
49
+ return value.map(_value => ({ key, value: _value.toString() }))
50
+ }
51
+ return [{ key, value: value.toString() }]
52
+ }
53
+ })
54
+ }
55
+
56
+ /**
57
+ * Checks if any parent filters are unselected or at their reset state.
58
+ * Returns true if at least one parent is not properly selected.
59
+ */
60
+ export const hasUnselectedParents = (parentParams, sharedFilters?: SharedFilter[]): boolean => {
61
+ if (!parentParams) return false
62
+
63
+ return parentParams.some(({ key, value }) => {
64
+ // Check if value is empty
65
+ if (value === '') return true
66
+
67
+ // Check if value equals the parent filter's resetLabel
68
+ if (sharedFilters) {
69
+ const parentFilter = sharedFilters.find(f => f.queryParameter === key || f.apiFilter?.valueSelector === key)
70
+ if (parentFilter?.resetLabel && value === parentFilter.resetLabel) {
71
+ return true
72
+ }
73
+ }
74
+
75
+ return false
76
+ })
77
+ }
78
+
79
+ // Keep old name for backward compatibility
80
+ export const notAllParentsSelected = hasUnselectedParents
81
+
82
+ export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
83
+ const { textSelector, valueSelector, subgroupTextSelector, subgroupValueSelector } = apiFilter
84
+ if (subgroupValueSelector) {
85
+ const memo = {}
86
+ data.forEach(v => {
87
+ if (!memo[v[valueSelector]]) {
88
+ memo[v[valueSelector]] = { text: v[textSelector || valueSelector], value: v[valueSelector], subOptions: [] }
89
+ }
90
+ memo[v[valueSelector]].subOptions.push({ text: v[subgroupTextSelector], value: v[subgroupValueSelector] })
91
+ })
92
+ return Object.values(memo)
93
+ } else {
94
+ }
95
+ return data.map(v => ({ text: v[textSelector || valueSelector], value: v[valueSelector] }))
96
+ }
97
+
98
+ /** API endpoint to fetch */
99
+ type Endpoint = string
100
+ type SharedFilterIndex = number
101
+ export const getToFetch = (
102
+ sharedFilters: SharedFilter[],
103
+ apiFilterDropdowns: APIFilterDropdowns
104
+ ): Record<Endpoint, [DropdownsKey, SharedFilterIndex]> => {
105
+ const toFetch = {}
106
+ sharedFilters.forEach((filter, index) => {
107
+ const baseEndpoint = filter.apiFilter?.apiEndpoint
108
+ if (!baseEndpoint) return
109
+ const _key = baseEndpoint
110
+ if (apiFilterDropdowns[_key]) return // don't reload cached filter
111
+ const parentParams = getParentParams(filter, sharedFilters)
112
+
113
+ if (notAllParentsSelected(parentParams, sharedFilters)) return // don't send request for dependent children filter options
114
+
115
+ const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
116
+ toFetch[endpoint] = [_key, index]
117
+ })
118
+ return toFetch
119
+ }
120
+
121
+ export const setActiveNestedDropdown = (dropdownOptions, sharedFilter) => {
122
+ const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
123
+ const defaultValue = dropdownOptions[0]?.value
124
+ const subDefaultValue = dropdownOptions[0]?.subOptions[0].value
125
+ const subDefaultQueryParamValue = getQueryParam(sharedFilter?.subGrouping.setByQueryParameter)
126
+ if (!sharedFilter.active) {
127
+ sharedFilter.active = defaultQueryParamValue || defaultValue
128
+ sharedFilter.subGrouping.active = subDefaultQueryParamValue || subDefaultValue
129
+ } else {
130
+ const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
131
+ sharedFilter.active = currentOption ? currentOption.value : defaultValue
132
+ if (currentOption) {
133
+ const currentSubOption = currentOption.subOptions.find(option => option.value === sharedFilter.subGrouping.active)
134
+ sharedFilter.subGrouping.active = currentSubOption?.value || subDefaultValue
135
+ } else {
136
+ sharedFilter.subGrouping.active = subDefaultValue
137
+ }
138
+ }
139
+ }
140
+
141
+ export const setActiveMultiDropdown = (dropdownOptions, sharedFilter) => {
142
+ const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
143
+ const multiDefaultQueryParamValue = Array.isArray(defaultQueryParamValue)
144
+ ? defaultQueryParamValue
145
+ : defaultQueryParamValue?.split(',')
146
+ const multiDefaultValue = defaultQueryParamValue ? multiDefaultQueryParamValue : [dropdownOptions[0]?.value]
147
+ const currentOption = (Array.isArray(sharedFilter.active) ? sharedFilter.active : []).filter(activeVal =>
148
+ dropdownOptions.find(option => option.value === activeVal)
149
+ )
150
+ sharedFilter.active = currentOption.length ? currentOption : multiDefaultValue
151
+ }
152
+
153
+ export const setAutoLoadDefaultValue = (
154
+ sharedFilterIndex: number,
155
+ dropdownOptions: DropdownOptions,
156
+ sharedFilters: SharedFilter[],
157
+ autoLoadFilterIndexes: number[]
158
+ ): SharedFilter => {
159
+ const sharedFiltersCopy = _.cloneDeep(sharedFilters)
160
+ const sharedFilter = _.cloneDeep(sharedFiltersCopy[sharedFilterIndex])
161
+ const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
162
+ const hasQueryParameter = sharedFilter.setByQueryParameter ? defaultQueryParamValue !== undefined : false
163
+ if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) {
164
+ if (hasQueryParameter && sharedFilter.apiFilter) {
165
+ const subQueryValue = getQueryParam(sharedFilter.subGrouping?.setByQueryParameter)
166
+ const isNestedDropdown = subQueryValue !== undefined
167
+ sharedFilter.queuedActive = isNestedDropdown ? [defaultQueryParamValue, subQueryValue] : defaultQueryParamValue
168
+ }
169
+ return sharedFilter // no autoLoading happening
170
+ }
171
+ if (autoLoadFilterIndexes.includes(sharedFilterIndex) || hasQueryParameter) {
172
+ const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
173
+ const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
174
+ if (filterParents && notAllParentFiltersSelected) return sharedFilter
175
+ if (sharedFilter.filterStyle === FILTER_STYLE.multiSelect) {
176
+ setActiveMultiDropdown(dropdownOptions, sharedFilter)
177
+ } else if (sharedFilter.filterStyle === FILTER_STYLE.nestedDropdown) {
178
+ setActiveNestedDropdown(dropdownOptions, sharedFilter)
179
+ } else {
180
+ const defaultValue = dropdownOptions[0]?.value
181
+ if (!sharedFilter.active) {
182
+ sharedFilter.active = defaultQueryParamValue || defaultValue
183
+ } else {
184
+ const currentOption = dropdownOptions.find(option => option.value == sharedFilter.active) // loose equality required: 2017 should equal '2017'
185
+ sharedFilter.active = currentOption ? currentOption.value : defaultValue
186
+ }
187
+ }
188
+ }
189
+ return sharedFilter
190
+ }
@@ -2,7 +2,11 @@ import _ from 'lodash'
2
2
  import { SharedFilter } from '../types/SharedFilter'
3
3
  import { FILTER_STYLE } from '../types/FilterStyles'
4
4
 
5
- const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) => {
5
+ /**
6
+ * Recursively calculates the tier/depth of a filter based on its parent dependencies.
7
+ * Root filters (no parents) are tier 1, children of root filters are tier 2, etc.
8
+ */
9
+ const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter): number => {
6
10
  if (!sharedFilter.parents?.length) {
7
11
  return 1
8
12
  } else {
@@ -12,6 +16,10 @@ const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) =>
12
16
  }
13
17
  }
14
18
 
19
+ /**
20
+ * Calculates and assigns tier values to all filters, returns the maximum tier.
21
+ * Mutates the filter objects by setting their tier property.
22
+ */
15
23
  function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
16
24
  let maxTier = 1
17
25
  filters.forEach(sharedFilter => {
@@ -23,7 +31,30 @@ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
23
31
  return maxTier
24
32
  }
25
33
 
26
- function filter(data = [], filters: SharedFilter[], condition) {
34
+ /**
35
+ * Checks if a filter is currently at its reset/incomplete state.
36
+ * A filter is incomplete if it's visible AND:
37
+ * - The active value is empty/null/undefined, OR
38
+ * - The active value equals the resetLabel (if one is defined)
39
+ */
40
+ export const isFilterAtResetState = (filter: SharedFilter): boolean => {
41
+ // Only check filters that are visible to the user
42
+ if (!filter.showDropdown) return false
43
+
44
+ // Check if active value is empty/null/undefined
45
+ const isEmptyValue = filter.active === '' || filter.active === null || filter.active === undefined
46
+
47
+ // Check if active value equals the resetLabel
48
+ const equalsResetLabel = filter.resetLabel && filter.resetLabel === filter.active
49
+
50
+ return isEmptyValue || equalsResetLabel
51
+ }
52
+
53
+ /**
54
+ * Filters data by applying filters of a specific tier.
55
+ * Filters are applied hierarchically by tier to handle parent-child dependencies.
56
+ */
57
+ function filterDataByTier(data = [], filters: SharedFilter[], tier: number) {
27
58
  const activeFilters = _.filter(filters, f => (f.resetLabel === f.active ? f.values?.includes(f.resetLabel) : true))
28
59
  return data.filter(row => {
29
60
  const foundMatchingFilter = activeFilters.find(filter => {
@@ -51,8 +82,9 @@ function filter(data = [], filters: SharedFilter[], condition) {
51
82
  isNotTheSelectedValue = subGroupActive && selectedSubGroupValue !== subGroupActive
52
83
  }
53
84
 
54
- const isFirstOccurrenceOfTier = filter.tier === condition
55
- if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
85
+ const isMatchingTier = filter.tier === tier
86
+ // Only apply client-side filtering for datafilter (urlfilters modify the API endpoint instead)
87
+ if (filter.type !== 'urlfilter' && isMatchingTier && isNotTheSelectedValue) {
56
88
  return true
57
89
  }
58
90
  })
@@ -60,17 +92,30 @@ function filter(data = [], filters: SharedFilter[], condition) {
60
92
  })
61
93
  }
62
94
 
95
+ /**
96
+ * Filters data based on shared filter configurations.
97
+ * Returns empty array if any filter is at its reset state (incomplete selection).
98
+ * Otherwise applies filters hierarchically by tier to handle parent-child dependencies.
99
+ */
63
100
  export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
64
101
  const maxTier = getMaxTierAndSetFilterTiers(filters)
65
102
 
103
+ // Check if any filters are currently at their reset state
104
+ const hasResetFilters = filters.some(isFilterAtResetState)
105
+
106
+ // If any filter is at reset state, return empty data to show "no data" message
107
+ if (hasResetFilters) {
108
+ return []
109
+ }
110
+
66
111
  for (let i = 0; i < maxTier; i++) {
67
112
  const lastIteration = i === maxTier - 1
68
113
 
69
- const filteredData = filter(_data, filters, i + 1)
114
+ const filteredData = filterDataByTier(_data, filters, i + 1)
70
115
 
71
116
  if (lastIteration) {
72
- // not sure if this last run of filter() function is necessary.
73
- return filter(filteredData, filters, maxTier - 1)
117
+ // not sure if this last run of filterDataByTier() function is necessary.
118
+ return filterDataByTier(filteredData, filters, maxTier - 1)
74
119
  }
75
120
  }
76
121
  }
@@ -0,0 +1,102 @@
1
+ import { SharedFilter } from '../types/SharedFilter'
2
+ import { APIFilterDropdowns } from '../components/DashboardFilters'
3
+ import { FILTER_STYLE } from '../types/FilterStyles'
4
+
5
+ /**
6
+ * Determines the reset value for a filter based on its configuration.
7
+ * When forceEmpty is true (like when clicking "Clear Filters"), always returns empty.
8
+ * Otherwise uses priority: defaultValue > empty string (for resetLabel) > first API option
9
+ * Note: resetLabel is for display purposes only. When present, we return empty string
10
+ * so the placeholder option renders correctly in the dropdown.
11
+ */
12
+ export const getFilterResetValue = (
13
+ filter: SharedFilter,
14
+ apiFilterDropdowns: APIFilterDropdowns,
15
+ forceEmpty: boolean = false
16
+ ): string | undefined => {
17
+ // When clearing filters, always reset to empty/resetLabel state
18
+ if (forceEmpty) {
19
+ // Return empty string to show reset label or placeholder, undefined falls back to first value
20
+ return ''
21
+ }
22
+
23
+ // If filter has a defaultValue, use that (for initial load)
24
+ if (filter.defaultValue) {
25
+ return filter.defaultValue
26
+ }
27
+ // If filter has a resetLabel, return empty string so placeholder renders
28
+ if (typeof filter.resetLabel === 'string') {
29
+ return ''
30
+ }
31
+ // Otherwise, use first available value if API filter
32
+ if (filter.apiFilter) {
33
+ const _key = filter.apiFilter.apiEndpoint
34
+ const options = apiFilterDropdowns[_key]
35
+ if (options && options.length > 0) {
36
+ return options[0].value
37
+ }
38
+ }
39
+ return undefined
40
+ }
41
+
42
+ /**
43
+ * Resets a filter's active and queuedActive values based on its filter style.
44
+ * Handles multi-select, nested-dropdown, and standard dropdown styles.
45
+ */
46
+ export const resetFilterToValue = (
47
+ filter: SharedFilter,
48
+ resetValue: string | undefined,
49
+ apiFilterDropdowns: APIFilterDropdowns
50
+ ): void => {
51
+ // Handle multi-select filters
52
+ if (filter.filterStyle === FILTER_STYLE.multiSelect) {
53
+ filter.active = resetValue ? [resetValue] : []
54
+ filter.queuedActive = undefined
55
+ } else if (filter.filterStyle === FILTER_STYLE.nestedDropdown) {
56
+ // For nested dropdowns, reset both group and subgroup
57
+ const _key = filter.apiFilter?.apiEndpoint
58
+ const options = apiFilterDropdowns[_key]
59
+
60
+ // When resetValue is explicitly empty/undefined, clear both group and subgroup
61
+ if (resetValue === '' || resetValue === undefined) {
62
+ filter.active = resetValue || ''
63
+ filter.subGrouping.active = ''
64
+ } else if (options && options.length > 0) {
65
+ // Use specific resetValue or fall back to first option
66
+ const selectedOption = options.find(opt => opt.value === resetValue) || options[0]
67
+ filter.active = selectedOption.value
68
+ if (selectedOption.subOptions && selectedOption.subOptions.length > 0) {
69
+ filter.subGrouping.active = selectedOption.subOptions[0].value
70
+ } else {
71
+ filter.subGrouping.active = ''
72
+ }
73
+ } else {
74
+ // No options available, use resetValue or empty
75
+ filter.active = resetValue || ''
76
+ filter.subGrouping.active = ''
77
+ }
78
+ filter.queuedActive = undefined
79
+ } else {
80
+ // Standard dropdown
81
+ filter.active = resetValue
82
+ filter.queuedActive = undefined
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Clears dropdown cache for child filters that have parent dependencies.
88
+ * Sets them to empty arrays so they appear disabled without loading state.
89
+ */
90
+ export const clearChildFilterDropdowns = (
91
+ sharedFilters: SharedFilter[],
92
+ apiFilterDropdowns: APIFilterDropdowns
93
+ ): APIFilterDropdowns => {
94
+ const updatedDropdowns = { ...apiFilterDropdowns }
95
+ sharedFilters.forEach(filter => {
96
+ if (filter.apiFilter && filter.parents && filter.parents.length > 0) {
97
+ // Set to empty array so they show as disabled without loading state
98
+ updatedDropdowns[filter.apiFilter.apiEndpoint] = []
99
+ }
100
+ })
101
+ return updatedDropdowns
102
+ }