@cdc/dashboard 4.26.4 → 4.26.5
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/CONFIG.md +77 -30
- package/LICENSE +201 -0
- package/dist/cdcdashboard.js +49936 -49166
- package/examples/dashboard-conditions-filters-incomplete.json +221 -0
- package/examples/dashboard-missing-datasets-multi.json +174 -0
- package/examples/dashboard-missing-datasets-single.json +121 -0
- package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
- package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
- package/examples/dashboard-stale-dataset-keys.json +181 -0
- package/examples/dashboard-tiered-filter-regression.json +190 -0
- package/examples/private/cfa-dashboard.json +651 -0
- package/examples/private/data-bite-wrap.json +6936 -0
- package/examples/private/multi-dash-fix.json +16963 -0
- package/examples/private/versions.json +41612 -0
- package/examples/us-map-filter-example.json +1074 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +6 -2
- package/src/CdcDashboardComponent.tsx +178 -87
- package/src/DashboardCopyPasteContext.test.tsx +33 -0
- package/src/DashboardCopyPasteContext.tsx +48 -0
- package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
- package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
- package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
- package/src/_stories/Dashboard.stories.tsx +294 -0
- package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
- package/src/components/Column.test.tsx +176 -0
- package/src/components/Column.tsx +214 -13
- package/src/components/DashboardConditionModal.test.tsx +420 -0
- package/src/components/DashboardConditionModal.tsx +367 -0
- package/src/components/DashboardConditionSummary.tsx +59 -0
- package/src/components/DashboardEditors.tsx +8 -0
- package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
- package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
- package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
- package/src/components/DataDesignerModal.tsx +2 -1
- package/src/components/Grid.tsx +8 -4
- package/src/components/Header/Header.tsx +36 -17
- package/src/components/Row.test.tsx +228 -0
- package/src/components/Row.tsx +93 -18
- package/src/components/VisualizationRow.test.tsx +396 -0
- package/src/components/VisualizationRow.tsx +110 -35
- package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
- package/src/components/Widget/Widget.test.tsx +218 -0
- package/src/components/Widget/Widget.tsx +119 -17
- package/src/components/Widget/widget.styles.css +31 -18
- package/src/components/dashboard-condition-modal.css +76 -0
- package/src/components/dashboard-condition-summary.css +87 -0
- package/src/helpers/addValuesToDashboardFilters.ts +3 -5
- package/src/helpers/addVisualization.ts +15 -4
- package/src/helpers/cloneDashboardWidget.ts +127 -0
- package/src/helpers/dashboardColumnWidgets.ts +99 -0
- package/src/helpers/dashboardConditionUi.ts +47 -0
- package/src/helpers/dashboardConditions.ts +200 -0
- package/src/helpers/dashboardFilterTargets.ts +156 -0
- package/src/helpers/filterData.ts +4 -9
- package/src/helpers/filterVisibility.ts +20 -0
- package/src/helpers/formatConfigBeforeSave.ts +2 -2
- package/src/helpers/getFilteredData.ts +18 -5
- package/src/helpers/getUpdateConfig.ts +43 -12
- package/src/helpers/getVizRowColumnLocator.ts +11 -1
- package/src/helpers/iconHash.tsx +9 -3
- package/src/helpers/mapDataToConfig.ts +31 -29
- package/src/helpers/reloadURLHelpers.ts +25 -5
- package/src/helpers/removeDashboardFilter.ts +33 -33
- package/src/helpers/tests/addVisualization.test.ts +53 -9
- package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
- package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
- package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
- package/src/helpers/tests/dashboardConditions.test.ts +428 -0
- package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
- package/src/helpers/tests/getFilteredData.test.ts +265 -86
- package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
- package/src/index.tsx +6 -3
- package/src/scss/grid.scss +249 -20
- package/src/scss/main.scss +108 -29
- package/src/store/dashboard.actions.ts +17 -4
- package/src/store/dashboard.reducer.test.ts +538 -0
- package/src/store/dashboard.reducer.ts +135 -22
- package/src/test/CdcDashboard.test.tsx +148 -0
- package/src/test/CdcDashboardComponent.test.tsx +935 -2
- package/src/types/ConfigRow.ts +15 -0
- package/src/types/DashboardFilters.ts +4 -0
- package/src/types/SharedFilter.ts +1 -0
|
@@ -11,8 +11,12 @@ import { MouseEventHandler } from 'react'
|
|
|
11
11
|
import Loader from '@cdc/core/components/Loader'
|
|
12
12
|
import Button from '@cdc/core/components/elements/Button'
|
|
13
13
|
import _ from 'lodash'
|
|
14
|
-
import {
|
|
14
|
+
import { getDropdownStyles } from '@cdc/core/components/Filters/components/Dropdown'
|
|
15
15
|
import Tabs from '@cdc/core/components/Filters/components/Tabs'
|
|
16
|
+
import FilterNote from '@cdc/core/components/Filters/components/FilterNote'
|
|
17
|
+
import parse from 'html-react-parser'
|
|
18
|
+
import './dashboardfilter.styles.css'
|
|
19
|
+
import { isVisibleDashboardFilter } from '../../helpers/filterVisibility'
|
|
16
20
|
|
|
17
21
|
type DashboardFilterProps = {
|
|
18
22
|
show: number[]
|
|
@@ -20,6 +24,7 @@ type DashboardFilterProps = {
|
|
|
20
24
|
apiFilterDropdowns: APIFilterDropdowns
|
|
21
25
|
handleOnChange: (index: number, value: string | string[]) => void
|
|
22
26
|
showSubmit: boolean
|
|
27
|
+
filterIntro?: string
|
|
23
28
|
applyFilters: MouseEventHandler<HTMLButtonElement>
|
|
24
29
|
applyFiltersButtonText?: string
|
|
25
30
|
handleReset?: MouseEventHandler<HTMLButtonElement>
|
|
@@ -31,6 +36,7 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
|
|
|
31
36
|
apiFilterDropdowns,
|
|
32
37
|
handleOnChange,
|
|
33
38
|
showSubmit,
|
|
39
|
+
filterIntro,
|
|
34
40
|
applyFilters,
|
|
35
41
|
applyFiltersButtonText,
|
|
36
42
|
handleReset
|
|
@@ -58,186 +64,198 @@ const DashboardFilters: React.FC<DashboardFilterProps> = ({
|
|
|
58
64
|
])
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
const visibleFilterIndexes = show.filter(filterIndex => isVisibleDashboardFilter(sharedFilters[filterIndex]))
|
|
68
|
+
const formClasses = [
|
|
69
|
+
'dashboard-filters__form',
|
|
70
|
+
'filters-section__wrapper',
|
|
71
|
+
visibleFilterIndexes.length > 1 ? 'filters-section__wrapper--multiple' : 'filters-section__wrapper--single'
|
|
72
|
+
]
|
|
73
|
+
|
|
61
74
|
return (
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
filter.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
75
|
+
<>
|
|
76
|
+
{filterIntro && <p className='filters-section__intro-text cove-prose mb-3 w-100'>{parse(filterIntro)}</p>}
|
|
77
|
+
<form className={formClasses.join(' ')}>
|
|
78
|
+
{show.map(filterIndex => {
|
|
79
|
+
const filter = sharedFilters[filterIndex]
|
|
80
|
+
|
|
81
|
+
if (!isVisibleDashboardFilter(filter))
|
|
82
|
+
return <React.Fragment key={`${filter?.key || 'missing'}-filtersection-${filterIndex}-option`} />
|
|
83
|
+
|
|
84
|
+
const label = stripDuplicateLabelIncrement(filter.key || '')
|
|
85
|
+
const values: JSX.Element[] = []
|
|
86
|
+
|
|
87
|
+
const _key = filter.apiFilter?.apiEndpoint
|
|
88
|
+
const loading = apiFilterDropdowns[_key] === null
|
|
89
|
+
|
|
90
|
+
const multiValues: { value; label }[] = []
|
|
91
|
+
const nestedOptions: NestedOptions = getNestedOptions({
|
|
92
|
+
orderedValues: filter.orderedValues,
|
|
93
|
+
values: filter.values,
|
|
94
|
+
subGrouping: filter.subGrouping
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if (_key && apiFilterDropdowns[_key]) {
|
|
98
|
+
// URL Filter
|
|
99
|
+
if (filter.filterStyle !== FILTER_STYLE.nestedDropdown) {
|
|
100
|
+
apiFilterDropdowns[_key].forEach(({ text, value }, index) => {
|
|
101
|
+
values.push(
|
|
102
|
+
<option key={`${value}-option-${index}`} value={value}>
|
|
103
|
+
{text}
|
|
104
|
+
</option>
|
|
105
|
+
)
|
|
106
|
+
multiValues.push({ value, label: text })
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// Data Filter
|
|
111
|
+
const orderedFilterValues = filter.orderedValues || filter.values
|
|
112
|
+
orderedFilterValues?.forEach((filterOption, index) => {
|
|
113
|
+
const labeledOpt = filter.labels && filter.labels[filterOption]
|
|
114
|
+
const resetLabelHasMatch = (filterOption || labeledOpt) === filter.resetLabel
|
|
115
|
+
|
|
116
|
+
if (!resetLabelHasMatch) {
|
|
117
|
+
values.push(
|
|
118
|
+
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
119
|
+
{labeledOpt || filterOption}
|
|
120
|
+
</option>
|
|
121
|
+
)
|
|
122
|
+
} else {
|
|
123
|
+
// add label to the front of list if it matches with reset label
|
|
124
|
+
values.unshift(
|
|
125
|
+
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
126
|
+
{labeledOpt || filterOption}
|
|
127
|
+
</option>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
multiValues.push({ value: filterOption, label: labeledOpt || filterOption })
|
|
97
132
|
})
|
|
98
133
|
}
|
|
99
|
-
} else {
|
|
100
|
-
// Data Filter
|
|
101
|
-
const orderedFilterValues = filter.orderedValues || filter.values
|
|
102
|
-
orderedFilterValues?.forEach((filterOption, index) => {
|
|
103
|
-
const labeledOpt = filter.labels && filter.labels[filterOption]
|
|
104
|
-
const resetLabelHasMatch = (filterOption || labeledOpt) === filter.resetLabel
|
|
105
|
-
|
|
106
|
-
if (!resetLabelHasMatch) {
|
|
107
|
-
values.push(
|
|
108
|
-
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
109
|
-
{labeledOpt || filterOption}
|
|
110
|
-
</option>
|
|
111
|
-
)
|
|
112
|
-
} else {
|
|
113
|
-
// add label to the front of list if it matches with reset label
|
|
114
|
-
values.unshift(
|
|
115
|
-
<option key={`${filter.key}-option-${index}`} value={filterOption}>
|
|
116
|
-
{labeledOpt || filterOption}
|
|
117
|
-
</option>
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
134
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
const isDisabled = !values.length
|
|
136
|
+
// push reset label only if it does not includes in filter values options
|
|
137
|
+
if (filter.resetLabel && !filter.values.includes(filter.resetLabel) && !_key) {
|
|
138
|
+
values.unshift(
|
|
139
|
+
<option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
|
|
140
|
+
{filter.resetLabel}
|
|
141
|
+
</option>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isTabSimple = filter.filterStyle === FILTER_STYLE.tabSimple
|
|
146
|
+
const dropdownStyles = getDropdownStyles(Boolean(filter.note?.trim()))
|
|
147
|
+
const formGroupClass = [
|
|
148
|
+
'dashboard-filters__field',
|
|
149
|
+
'form-group',
|
|
150
|
+
loading ? 'loading-filter' : '',
|
|
151
|
+
isTabSimple ? 'w-100' : ''
|
|
152
|
+
]
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.join(' ')
|
|
155
|
+
return (
|
|
156
|
+
<div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
|
|
157
|
+
{label && (
|
|
158
|
+
<label className='font-weight-bold mb-2' htmlFor={`filter-${filterIndex}`}>
|
|
159
|
+
{label}
|
|
160
|
+
</label>
|
|
161
|
+
)}
|
|
162
|
+
<FilterNote note={filter.note} />
|
|
163
|
+
{filter.filterStyle === FILTER_STYLE.tabSimple ? (
|
|
164
|
+
<Tabs
|
|
165
|
+
filter={filter}
|
|
166
|
+
index={filterIndex}
|
|
167
|
+
changeFilterActive={(index, value) => handleOnChange(index, value)}
|
|
168
|
+
loading={loading}
|
|
169
|
+
/>
|
|
170
|
+
) : filter.filterStyle === FILTER_STYLE.multiSelect ? (
|
|
171
|
+
<MultiSelect
|
|
172
|
+
label={label}
|
|
173
|
+
options={multiValues}
|
|
174
|
+
fieldName={filterIndex}
|
|
175
|
+
updateField={updateField}
|
|
176
|
+
selected={filter.active as string[]}
|
|
177
|
+
limit={filter.selectLimit || 5}
|
|
178
|
+
loading={loading}
|
|
179
|
+
/>
|
|
180
|
+
) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
|
|
181
|
+
<NestedDropdown
|
|
182
|
+
activeGroup={(filter.queuedActive?.[0] || filter.active) as string}
|
|
183
|
+
activeSubGroup={(filter.queuedActive?.[1] || filter.subGrouping?.active) as string}
|
|
184
|
+
displaySubgroupingOnly={filter.displaySubgroupingOnly}
|
|
185
|
+
filterIndex={filterIndex}
|
|
186
|
+
options={_key ? getNestedDropdownOptions(apiFilterDropdowns[_key]) : nestedOptions}
|
|
187
|
+
listLabel={label}
|
|
188
|
+
handleSelectedItems={value => updateField(null, null, filterIndex, value)}
|
|
189
|
+
loading={loading}
|
|
190
|
+
/>
|
|
191
|
+
) : filter.filterStyle === FILTER_STYLE.combobox ? (
|
|
192
|
+
<ComboBox
|
|
193
|
+
options={multiValues}
|
|
194
|
+
fieldName={filterIndex}
|
|
195
|
+
updateField={updateField}
|
|
196
|
+
selected={(filter.queuedActive || filter.active) as string}
|
|
197
|
+
label={label}
|
|
198
|
+
loading={loading}
|
|
199
|
+
placeholder={filter.resetLabel || '- Select -'}
|
|
200
|
+
/>
|
|
201
|
+
) : (
|
|
202
|
+
<>
|
|
203
|
+
<select
|
|
204
|
+
id={`filter-${filterIndex}`}
|
|
205
|
+
className={`cove-form-select ${dropdownStyles}`}
|
|
206
|
+
data-index='0'
|
|
207
|
+
value={loading ? 'Loading...' : filter.queuedActive || filter.active}
|
|
208
|
+
onChange={val => {
|
|
209
|
+
handleOnChange(filterIndex, val.target.value)
|
|
210
|
+
}}
|
|
211
|
+
disabled={loading || isDisabled}
|
|
212
|
+
>
|
|
213
|
+
{loading && <option value='Loading...'>Loading...</option>}
|
|
214
|
+
{/* For API filters, show placeholder when no value is selected */}
|
|
215
|
+
{_key && nullVal(filter) && (
|
|
216
|
+
<option key={`reset-label`} value=''>
|
|
217
|
+
{filter.resetLabel || '- Select One -'}
|
|
218
|
+
</option>
|
|
219
|
+
)}
|
|
220
|
+
{/* For non-API filters or when no value is selected, show empty option */}
|
|
221
|
+
{!_key && nullVal(filter) && (
|
|
222
|
+
<option key={`select`} value=''>
|
|
223
|
+
{filter.resetLabel || '- Select -'}
|
|
224
|
+
</option>
|
|
225
|
+
)}
|
|
226
|
+
{values}
|
|
227
|
+
</select>
|
|
228
|
+
{loading && <Loader spinnerType={'text-secondary'} />}
|
|
229
|
+
</>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
132
232
|
)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<MultiSelect
|
|
155
|
-
label={label}
|
|
156
|
-
options={multiValues}
|
|
157
|
-
fieldName={filterIndex}
|
|
158
|
-
updateField={updateField}
|
|
159
|
-
selected={filter.active as string[]}
|
|
160
|
-
limit={filter.selectLimit || 5}
|
|
161
|
-
loading={loading}
|
|
162
|
-
/>
|
|
163
|
-
) : filter.filterStyle === FILTER_STYLE.nestedDropdown ? (
|
|
164
|
-
<NestedDropdown
|
|
165
|
-
activeGroup={(filter.queuedActive?.[0] || filter.active) as string}
|
|
166
|
-
activeSubGroup={(filter.queuedActive?.[1] || filter.subGrouping?.active) as string}
|
|
167
|
-
displaySubgroupingOnly={filter.displaySubgroupingOnly}
|
|
168
|
-
filterIndex={filterIndex}
|
|
169
|
-
options={_key ? getNestedDropdownOptions(apiFilterDropdowns[_key]) : nestedOptions}
|
|
170
|
-
listLabel={label}
|
|
171
|
-
handleSelectedItems={value => updateField(null, null, filterIndex, value)}
|
|
172
|
-
loading={loading}
|
|
173
|
-
/>
|
|
174
|
-
) : filter.filterStyle === FILTER_STYLE.combobox ? (
|
|
175
|
-
<ComboBox
|
|
176
|
-
options={multiValues}
|
|
177
|
-
fieldName={filterIndex}
|
|
178
|
-
updateField={updateField}
|
|
179
|
-
selected={(filter.queuedActive || filter.active) as string}
|
|
180
|
-
label={label}
|
|
181
|
-
loading={loading}
|
|
182
|
-
placeholder={filter.resetLabel || '- Select -'}
|
|
183
|
-
/>
|
|
184
|
-
) : (
|
|
185
|
-
<>
|
|
186
|
-
<select
|
|
187
|
-
id={`filter-${filterIndex}`}
|
|
188
|
-
className={`cove-form-select ${DROPDOWN_STYLES}`}
|
|
189
|
-
data-index='0'
|
|
190
|
-
value={loading ? 'Loading...' : filter.queuedActive || filter.active}
|
|
191
|
-
onChange={val => {
|
|
192
|
-
handleOnChange(filterIndex, val.target.value)
|
|
193
|
-
}}
|
|
194
|
-
disabled={loading || isDisabled}
|
|
195
|
-
>
|
|
196
|
-
{loading && <option value='Loading...'>Loading...</option>}
|
|
197
|
-
{/* For API filters, show placeholder when no value is selected */}
|
|
198
|
-
{_key && nullVal(filter) && (
|
|
199
|
-
<option key={`reset-label`} value=''>
|
|
200
|
-
{filter.resetLabel || '- Select One -'}
|
|
201
|
-
</option>
|
|
202
|
-
)}
|
|
203
|
-
{/* For non-API filters or when no value is selected, show empty option */}
|
|
204
|
-
{!_key && nullVal(filter) && (
|
|
205
|
-
<option key={`select`} value=''>
|
|
206
|
-
{filter.resetLabel || '- Select -'}
|
|
207
|
-
</option>
|
|
208
|
-
)}
|
|
209
|
-
{values}
|
|
210
|
-
</select>
|
|
211
|
-
{loading && <Loader spinnerType={'text-secondary'} />}
|
|
212
|
-
</>
|
|
233
|
+
})}
|
|
234
|
+
{showSubmit && (
|
|
235
|
+
<div className='dashboard-filters__actions'>
|
|
236
|
+
<Button
|
|
237
|
+
variant='primary'
|
|
238
|
+
className='mb-1 me-2'
|
|
239
|
+
onClick={applyFilters}
|
|
240
|
+
disabled={visibleFilterIndexes.some(filterIndex => {
|
|
241
|
+
const emptyFilterValues = [undefined, '', '- Select -']
|
|
242
|
+
return (
|
|
243
|
+
emptyFilterValues.includes(sharedFilters[filterIndex]?.queuedActive) &&
|
|
244
|
+
emptyFilterValues.includes(sharedFilters[filterIndex]?.active)
|
|
245
|
+
)
|
|
246
|
+
})}
|
|
247
|
+
>
|
|
248
|
+
{applyFiltersButtonText || 'GO!'}
|
|
249
|
+
</Button>
|
|
250
|
+
{handleReset && (
|
|
251
|
+
<Button variant='link' className='mb-1' onClick={handleReset}>
|
|
252
|
+
Clear Filters
|
|
253
|
+
</Button>
|
|
213
254
|
)}
|
|
214
255
|
</div>
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<div className='dashboard-filters__actions'>
|
|
219
|
-
<Button
|
|
220
|
-
variant='primary'
|
|
221
|
-
className='mb-1 me-2'
|
|
222
|
-
onClick={applyFilters}
|
|
223
|
-
disabled={show.some(filterIndex => {
|
|
224
|
-
const emptyFilterValues = [undefined, '', '- Select -']
|
|
225
|
-
return (
|
|
226
|
-
emptyFilterValues.includes(sharedFilters[filterIndex].queuedActive) &&
|
|
227
|
-
emptyFilterValues.includes(sharedFilters[filterIndex].active)
|
|
228
|
-
)
|
|
229
|
-
})}
|
|
230
|
-
>
|
|
231
|
-
{applyFiltersButtonText || 'GO!'}
|
|
232
|
-
</Button>
|
|
233
|
-
{handleReset && (
|
|
234
|
-
<Button variant='link' className='mb-1' onClick={handleReset}>
|
|
235
|
-
Clear Filters
|
|
236
|
-
</Button>
|
|
237
|
-
)}
|
|
238
|
-
</div>
|
|
239
|
-
)}
|
|
240
|
-
</form>
|
|
256
|
+
)}
|
|
257
|
+
</form>
|
|
258
|
+
</>
|
|
241
259
|
)
|
|
242
260
|
}
|
|
243
261
|
|
package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { DashboardContext, DashboardDispatchContext, initialState } from '../../../DashboardContext'
|
|
5
|
+
import { GlobalContext } from '@cdc/core/components/GlobalContext'
|
|
6
|
+
import DashboardFiltersEditor from './DashboardFiltersEditor'
|
|
7
|
+
|
|
8
|
+
vi.mock('@cdc/core/components/ui/Icon', () => ({
|
|
9
|
+
default: props => <span data-testid='mock-icon' {...props} />
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('@hello-pangea/dnd', () => ({
|
|
13
|
+
DragDropContext: ({ children }) => <div>{children}</div>,
|
|
14
|
+
Droppable: ({ children }) => <div>{children({ droppableProps: {}, innerRef: vi.fn(), placeholder: null })}</div>,
|
|
15
|
+
Draggable: ({ children }) => (
|
|
16
|
+
<div>
|
|
17
|
+
{children(
|
|
18
|
+
{
|
|
19
|
+
draggableProps: { style: {} },
|
|
20
|
+
dragHandleProps: {},
|
|
21
|
+
innerRef: vi.fn()
|
|
22
|
+
},
|
|
23
|
+
{ isDragging: false }
|
|
24
|
+
)}
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
const renderEditor = (visual = { grayBackground: false }, sharedFilters = [], sharedFilterIndexes = []) => {
|
|
30
|
+
const updateConfig = vi.fn()
|
|
31
|
+
const dispatch = vi.fn()
|
|
32
|
+
const vizConfig = {
|
|
33
|
+
uid: 'dashboardFilters1',
|
|
34
|
+
type: 'dashboardFilters',
|
|
35
|
+
visualizationType: 'dashboardFilters',
|
|
36
|
+
filterBehavior: 'Filter Change',
|
|
37
|
+
filterIntro: '',
|
|
38
|
+
sharedFilterIndexes,
|
|
39
|
+
visual
|
|
40
|
+
} as any
|
|
41
|
+
|
|
42
|
+
const rendered = render(
|
|
43
|
+
<GlobalContext.Provider
|
|
44
|
+
value={{
|
|
45
|
+
overlay: {
|
|
46
|
+
object: null,
|
|
47
|
+
show: false,
|
|
48
|
+
disableBgClose: false,
|
|
49
|
+
actions: {
|
|
50
|
+
openOverlay: vi.fn(),
|
|
51
|
+
toggleOverlay: vi.fn()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<DashboardContext.Provider
|
|
57
|
+
value={{
|
|
58
|
+
...initialState,
|
|
59
|
+
config: {
|
|
60
|
+
type: 'dashboard',
|
|
61
|
+
dashboard: { sharedFilters },
|
|
62
|
+
datasets: {},
|
|
63
|
+
rows: [],
|
|
64
|
+
visualizations: {
|
|
65
|
+
dashboardFilters1: vizConfig
|
|
66
|
+
}
|
|
67
|
+
} as any,
|
|
68
|
+
data: {},
|
|
69
|
+
outerContainerRef: vi.fn(),
|
|
70
|
+
setParentConfig: vi.fn(),
|
|
71
|
+
isDebug: false,
|
|
72
|
+
isEditor: true,
|
|
73
|
+
reloadURLData: vi.fn(),
|
|
74
|
+
loadAPIFilters: vi.fn(),
|
|
75
|
+
setAPIFilterDropdowns: vi.fn(),
|
|
76
|
+
setAPILoading: vi.fn()
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<DashboardDispatchContext.Provider value={dispatch}>
|
|
80
|
+
<DashboardFiltersEditor updateConfig={updateConfig} vizConfig={vizConfig} />
|
|
81
|
+
</DashboardDispatchContext.Provider>
|
|
82
|
+
</DashboardContext.Provider>
|
|
83
|
+
</GlobalContext.Provider>
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return { ...rendered, dispatch, updateConfig, vizConfig }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('DashboardFiltersEditor', () => {
|
|
90
|
+
it('renders a Visual accordion with a Gray Background option', () => {
|
|
91
|
+
renderEditor()
|
|
92
|
+
|
|
93
|
+
expect(screen.getByText('Visual')).toBeInTheDocument()
|
|
94
|
+
expect(screen.getAllByLabelText('Use Gray Background Style')[0]).not.toBeChecked()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('updates visual.grayBackground when Gray Background is toggled', () => {
|
|
98
|
+
const { updateConfig, vizConfig } = renderEditor()
|
|
99
|
+
|
|
100
|
+
fireEvent.click(screen.getAllByLabelText('Use Gray Background Style')[0])
|
|
101
|
+
|
|
102
|
+
expect(updateConfig).toHaveBeenCalledWith({
|
|
103
|
+
...vizConfig,
|
|
104
|
+
visual: {
|
|
105
|
+
grayBackground: true
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('updates filterIntro from the General panel', async () => {
|
|
111
|
+
const { updateConfig, vizConfig } = renderEditor()
|
|
112
|
+
|
|
113
|
+
fireEvent.change(screen.getByLabelText('Filter intro text'), {
|
|
114
|
+
target: { value: 'Choose filters before viewing results.' }
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(updateConfig).toHaveBeenCalledWith({
|
|
119
|
+
...vizConfig,
|
|
120
|
+
filterIntro: 'Choose filters before viewing results.'
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it.each([
|
|
126
|
+
['combobox', 'tab-simple', 'Show'],
|
|
127
|
+
['tab-simple', 'combobox', 'Show'],
|
|
128
|
+
['dropdown', 'multi-select', ['Show']]
|
|
129
|
+
])('applies the configured default when switching a data filter from %s to %s', (initialStyle, nextStyle, active) => {
|
|
130
|
+
const sharedFilter = {
|
|
131
|
+
key: 'Status',
|
|
132
|
+
type: 'datafilter',
|
|
133
|
+
filterStyle: initialStyle,
|
|
134
|
+
showDropdown: true,
|
|
135
|
+
values: ['Show', 'Hide'],
|
|
136
|
+
orderedValues: ['Show', 'Hide'],
|
|
137
|
+
columnName: 'status',
|
|
138
|
+
defaultValue: 'Show',
|
|
139
|
+
active: '',
|
|
140
|
+
order: 'cust'
|
|
141
|
+
}
|
|
142
|
+
const { container, dispatch } = renderEditor({ grayBackground: false }, [sharedFilter], [0])
|
|
143
|
+
|
|
144
|
+
fireEvent.click(container.querySelector('.editor-field-item__header button') as HTMLButtonElement)
|
|
145
|
+
fireEvent.change(screen.getAllByLabelText('Filter Style')[0], { target: { value: nextStyle } })
|
|
146
|
+
|
|
147
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
148
|
+
type: 'SET_SHARED_FILTERS',
|
|
149
|
+
payload: [
|
|
150
|
+
{
|
|
151
|
+
...sharedFilter,
|
|
152
|
+
active,
|
|
153
|
+
apiFilter: {
|
|
154
|
+
apiEndpoint: '',
|
|
155
|
+
subgroupValueSelector: '',
|
|
156
|
+
textSelector: '',
|
|
157
|
+
valueSelector: ''
|
|
158
|
+
},
|
|
159
|
+
filterStyle: nextStyle
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -55,6 +55,11 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
55
55
|
const [canAddExisting, setCanAddExisting] = useState(false)
|
|
56
56
|
const [isNestedDragHovered, setIsNestedDragHovered] = useState(false)
|
|
57
57
|
|
|
58
|
+
const getActiveValueForFilterStyle = (filter: SharedFilter, filterStyle: string) => {
|
|
59
|
+
const defaultValue = filter.defaultValue || filter.values?.[0] || ''
|
|
60
|
+
return filterStyle === FILTER_STYLE.multiSelect ? (defaultValue ? [defaultValue] : []) : defaultValue
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
const updateFilterProp = (prop: string, index: number, value) => {
|
|
59
64
|
const newSharedFilters = cloneDeep(sharedFilters)
|
|
60
65
|
const {
|
|
@@ -81,7 +86,7 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
81
86
|
} else if (prop === 'filterStyle') {
|
|
82
87
|
newSharedFilters[index] = {
|
|
83
88
|
...newSharedFilters[index],
|
|
84
|
-
active:
|
|
89
|
+
active: getActiveValueForFilterStyle(newSharedFilters[index], value),
|
|
85
90
|
apiFilter: {
|
|
86
91
|
apiEndpoint: '',
|
|
87
92
|
subgroupValueSelector: '',
|
|
@@ -188,6 +193,15 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
188
193
|
</Tooltip>
|
|
189
194
|
}
|
|
190
195
|
/>
|
|
196
|
+
<TextField
|
|
197
|
+
type='textarea'
|
|
198
|
+
className='filter-editor__compact-textarea'
|
|
199
|
+
label='Filter intro text'
|
|
200
|
+
value={vizConfig.filterIntro || ''}
|
|
201
|
+
updateField={(_section, _subsection, _key, value) => {
|
|
202
|
+
updateConfig({ ...vizConfig, filterIntro: value })
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
191
205
|
{vizConfig.filterBehavior === 'Apply Button' && (
|
|
192
206
|
<TextField
|
|
193
207
|
label='Apply Filter Button Text'
|
|
@@ -244,6 +258,29 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
244
258
|
</AccordionItemPanel>
|
|
245
259
|
</AccordionItem>
|
|
246
260
|
|
|
261
|
+
<AccordionItem>
|
|
262
|
+
<AccordionItemHeading>
|
|
263
|
+
<AccordionItemButton>Visual</AccordionItemButton>
|
|
264
|
+
</AccordionItemHeading>
|
|
265
|
+
<AccordionItemPanel>
|
|
266
|
+
<CheckBox
|
|
267
|
+
label='Use Gray Background Style'
|
|
268
|
+
section='visual'
|
|
269
|
+
fieldName='grayBackground'
|
|
270
|
+
value={vizConfig.visual?.grayBackground ?? false}
|
|
271
|
+
updateField={(_section, _subsection, _key, value) => {
|
|
272
|
+
updateConfig({
|
|
273
|
+
...vizConfig,
|
|
274
|
+
visual: {
|
|
275
|
+
...vizConfig.visual,
|
|
276
|
+
grayBackground: value
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}}
|
|
280
|
+
/>
|
|
281
|
+
</AccordionItemPanel>
|
|
282
|
+
</AccordionItem>
|
|
283
|
+
|
|
247
284
|
<AccordionItem>
|
|
248
285
|
<AccordionItemHeading>
|
|
249
286
|
<AccordionItemButton>Filters</AccordionItemButton>
|
|
@@ -339,9 +376,11 @@ const DashboardFiltersEditor: React.FC<DashboardFitlersEditorProps> = ({ vizConf
|
|
|
339
376
|
value={''}
|
|
340
377
|
options={[{ value: '', label: 'Select' }, ...(existingOptions || [])]}
|
|
341
378
|
onChange={e => {
|
|
379
|
+
const parsed = Number(e.target.value)
|
|
380
|
+
if (!e.target.value || isNaN(parsed)) return
|
|
342
381
|
updateConfig({
|
|
343
382
|
...vizConfig,
|
|
344
|
-
sharedFilterIndexes: [...vizConfig.sharedFilterIndexes,
|
|
383
|
+
sharedFilterIndexes: [...vizConfig.sharedFilterIndexes, parsed]
|
|
345
384
|
})
|
|
346
385
|
setCanAddExisting(false)
|
|
347
386
|
}}
|