@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.
Files changed (91) hide show
  1. package/CONFIG.md +77 -30
  2. package/LICENSE +201 -0
  3. package/dist/cdcdashboard.js +49936 -49166
  4. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  5. package/examples/dashboard-missing-datasets-multi.json +174 -0
  6. package/examples/dashboard-missing-datasets-single.json +121 -0
  7. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  8. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  9. package/examples/dashboard-stale-dataset-keys.json +181 -0
  10. package/examples/dashboard-tiered-filter-regression.json +190 -0
  11. package/examples/private/cfa-dashboard.json +651 -0
  12. package/examples/private/data-bite-wrap.json +6936 -0
  13. package/examples/private/multi-dash-fix.json +16963 -0
  14. package/examples/private/versions.json +41612 -0
  15. package/examples/us-map-filter-example.json +1074 -0
  16. package/package.json +9 -9
  17. package/src/CdcDashboard.tsx +6 -2
  18. package/src/CdcDashboardComponent.tsx +178 -87
  19. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  20. package/src/DashboardCopyPasteContext.tsx +48 -0
  21. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  22. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  23. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  24. package/src/_stories/Dashboard.stories.tsx +294 -0
  25. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  26. package/src/components/Column.test.tsx +176 -0
  27. package/src/components/Column.tsx +214 -13
  28. package/src/components/DashboardConditionModal.test.tsx +420 -0
  29. package/src/components/DashboardConditionModal.tsx +367 -0
  30. package/src/components/DashboardConditionSummary.tsx +59 -0
  31. package/src/components/DashboardEditors.tsx +8 -0
  32. package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
  33. package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
  34. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  35. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
  36. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
  37. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
  38. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  39. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  40. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  41. package/src/components/DataDesignerModal.tsx +2 -1
  42. package/src/components/Grid.tsx +8 -4
  43. package/src/components/Header/Header.tsx +36 -17
  44. package/src/components/Row.test.tsx +228 -0
  45. package/src/components/Row.tsx +93 -18
  46. package/src/components/VisualizationRow.test.tsx +396 -0
  47. package/src/components/VisualizationRow.tsx +110 -35
  48. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  49. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  50. package/src/components/Widget/Widget.test.tsx +218 -0
  51. package/src/components/Widget/Widget.tsx +119 -17
  52. package/src/components/Widget/widget.styles.css +31 -18
  53. package/src/components/dashboard-condition-modal.css +76 -0
  54. package/src/components/dashboard-condition-summary.css +87 -0
  55. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  56. package/src/helpers/addVisualization.ts +15 -4
  57. package/src/helpers/cloneDashboardWidget.ts +127 -0
  58. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  59. package/src/helpers/dashboardConditionUi.ts +47 -0
  60. package/src/helpers/dashboardConditions.ts +200 -0
  61. package/src/helpers/dashboardFilterTargets.ts +156 -0
  62. package/src/helpers/filterData.ts +4 -9
  63. package/src/helpers/filterVisibility.ts +20 -0
  64. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  65. package/src/helpers/getFilteredData.ts +18 -5
  66. package/src/helpers/getUpdateConfig.ts +43 -12
  67. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  68. package/src/helpers/iconHash.tsx +9 -3
  69. package/src/helpers/mapDataToConfig.ts +31 -29
  70. package/src/helpers/reloadURLHelpers.ts +25 -5
  71. package/src/helpers/removeDashboardFilter.ts +33 -33
  72. package/src/helpers/tests/addVisualization.test.ts +53 -9
  73. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  74. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  75. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  76. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  77. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  78. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  79. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  80. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  81. package/src/index.tsx +6 -3
  82. package/src/scss/grid.scss +249 -20
  83. package/src/scss/main.scss +108 -29
  84. package/src/store/dashboard.actions.ts +17 -4
  85. package/src/store/dashboard.reducer.test.ts +538 -0
  86. package/src/store/dashboard.reducer.ts +135 -22
  87. package/src/test/CdcDashboard.test.tsx +148 -0
  88. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  89. package/src/types/ConfigRow.ts +15 -0
  90. package/src/types/DashboardFilters.ts +4 -0
  91. 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 { DROPDOWN_STYLES } from '@cdc/core/components/Filters/components/Dropdown'
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
- <form className='d-flex flex-wrap'>
63
- {show.map(filterIndex => {
64
- const filter = sharedFilters[filterIndex]
65
- const urlFilterType = filter.type === 'urlfilter'
66
- const label = stripDuplicateLabelIncrement(filter.key || '')
67
-
68
- if (
69
- !urlFilterType &&
70
- !filter.showDropdown &&
71
- filter.filterStyle !== FILTER_STYLE.nestedDropdown &&
72
- filter.filterStyle !== FILTER_STYLE.tabSimple
73
- )
74
- return <React.Fragment key={`${filter.key}-filtersection-${filterIndex}-option`} />
75
- const values: JSX.Element[] = []
76
-
77
- const _key = filter.apiFilter?.apiEndpoint
78
- const loading = apiFilterDropdowns[_key] === null
79
-
80
- const multiValues: { value; label }[] = []
81
- const nestedOptions: NestedOptions = getNestedOptions({
82
- orderedValues: filter.orderedValues,
83
- values: filter.values,
84
- subGrouping: filter.subGrouping
85
- })
86
-
87
- if (_key && apiFilterDropdowns[_key]) {
88
- // URL Filter
89
- if (filter.filterStyle !== FILTER_STYLE.nestedDropdown) {
90
- apiFilterDropdowns[_key].forEach(({ text, value }, index) => {
91
- values.push(
92
- <option key={`${value}-option-${index}`} value={value}>
93
- {text}
94
- </option>
95
- )
96
- multiValues.push({ value, label: text })
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
- multiValues.push({ value: filterOption, label: labeledOpt || filterOption })
122
- })
123
- }
124
-
125
- const isDisabled = !values.length
126
- // push reset label only if it does not includes in filter values options
127
- if (filter.resetLabel && !filter.values.includes(filter.resetLabel) && !_key) {
128
- values.unshift(
129
- <option key={`${filter.resetLabel}-option`} value={filter.resetLabel}>
130
- {filter.resetLabel}
131
- </option>
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
- const isTabSimple = filter.filterStyle === FILTER_STYLE.tabSimple
136
- const formGroupClass = `form-group${isTabSimple ? '' : ' me-4'} mb-1${loading ? ' loading-filter' : ''}${
137
- isTabSimple ? ' w-100' : ''
138
- }`
139
- return (
140
- <div className={formGroupClass} key={`${filter.key}-filtersection-${filterIndex}`}>
141
- {label && (
142
- <label className='font-weight-bold mb-2' htmlFor={`filter-${filterIndex}`}>
143
- {label}
144
- </label>
145
- )}
146
- {filter.filterStyle === FILTER_STYLE.tabSimple ? (
147
- <Tabs
148
- filter={filter}
149
- index={filterIndex}
150
- changeFilterActive={(index, value) => handleOnChange(index, value)}
151
- loading={loading}
152
- />
153
- ) : filter.filterStyle === FILTER_STYLE.multiSelect ? (
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
- {showSubmit && (
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
 
@@ -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, e.target.value]
383
+ sharedFilterIndexes: [...vizConfig.sharedFilterIndexes, parsed]
345
384
  })
346
385
  setCanAddExisting(false)
347
386
  }}