@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.
- package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
- package/dist/cdcdashboard.js +80040 -75976
- package/examples/api-test/categories.json +18 -0
- package/examples/api-test/chart-data.json +602 -0
- package/examples/api-test/topics.json +47 -0
- package/examples/api-test/years.json +22 -0
- package/examples/markup-axis-label.json +4167 -0
- package/examples/private/DEV-10538.json +407 -0
- package/examples/private/DEV-11405.json +39112 -0
- package/examples/private/big-dashboard.json +39112 -0
- package/examples/private/brfs-2.json +1532 -0
- package/examples/private/brfs.json +2128 -2138
- package/examples/private/clade-2.json +430 -0
- package/examples/private/delete.json +32919 -0
- package/examples/private/diabetes.json +5582 -0
- package/examples/private/example-2.json +49796 -0
- package/examples/private/group-legend-test.json +328 -0
- package/examples/private/map.json +1211 -0
- package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +14041 -0
- package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +14041 -0
- package/examples/private/markup-footer/burden_toolkit_mortality_qaly_data.csv +18721 -0
- package/examples/private/markup-footer/burden_toolkit_mortality_yll_data.csv +18721 -0
- package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
- package/examples/private/markup-variables.json +1451 -0
- package/examples/private/markup.json +5471 -0
- package/examples/private/mpox.json +38128 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/ophdst.json +38754 -0
- package/examples/private/pedro.json +1 -0
- package/examples/private/pivot.json +683 -0
- package/examples/private/reset.json +32920 -0
- package/examples/private/sewershed.json +435 -0
- package/examples/private/tobacco.json +1938 -0
- package/examples/test-api-filter-reset.json +132 -0
- package/index.html +2 -2
- package/package.json +16 -10
- package/src/CdcDashboard.tsx +1 -3
- package/src/CdcDashboardComponent.tsx +34 -16
- package/src/DashboardContext.tsx +5 -1
- package/src/_stories/Dashboard.API.stories.tsx +62 -0
- package/src/_stories/Dashboard.stories.tsx +492 -472
- package/src/_stories/_mock/api/cessation.json +1 -0
- package/src/_stories/_mock/api/data-explorer.json +1 -0
- package/src/_stories/_mock/api/explore-by-location.json +1 -0
- package/src/_stories/_mock/api/explore-by-topic.json +1 -0
- package/src/_stories/_mock/api/legislation.json +1 -0
- package/src/_stories/_mock/api/oral-health-data.json +1 -0
- package/src/_stories/_mock/custom-order-new-values.json +116 -0
- package/src/components/CollapsibleVisualizationRow.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +34 -23
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +29 -12
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +81 -112
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +82 -52
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +130 -31
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +80 -21
- package/src/components/DataDesignerModal.tsx +227 -210
- package/src/components/Header/Header.tsx +13 -12
- package/src/components/Toggle/Toggle.tsx +48 -47
- package/src/components/VisualizationRow.tsx +13 -6
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -0
- package/src/components/Widget/Widget.tsx +47 -18
- package/src/helpers/addValuesToDashboardFilters.ts +111 -60
- package/src/helpers/apiFilterHelpers.ts +190 -166
- package/src/helpers/filterData.ts +52 -7
- package/src/helpers/filterResetHelpers.ts +102 -0
- package/src/helpers/formatConfigBeforeSave.ts +137 -0
- package/src/helpers/getVizConfig.ts +36 -18
- package/src/helpers/loadAPIFilters.ts +109 -99
- package/src/helpers/reloadURLHelpers.ts +1 -1
- package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
- package/src/helpers/tests/formatConfigBeforeSave.test.ts +69 -0
- package/src/index.tsx +1 -1
- package/src/scss/editor-panel.scss +3 -431
- package/src/scss/grid.scss +7 -5
- package/src/scss/main.scss +1 -24
- package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
- package/src/types/DashboardFilters.ts +9 -8
- package/src/types/InitialState.ts +12 -12
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/coreStyles_dashboard.scss +0 -3
- package/src/helpers/getAutoLoadVisualization.ts +0 -11
- package/src/scss/mixins.scss +0 -47
- package/src/scss/variables.scss +0 -5
- /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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
)
|
|
126
|
-
sharedFilter.active
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
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 =
|
|
114
|
+
const filteredData = filterDataByTier(_data, filters, i + 1)
|
|
70
115
|
|
|
71
116
|
if (lastIteration) {
|
|
72
|
-
// not sure if this last run of
|
|
73
|
-
return
|
|
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
|
+
}
|