@cdc/dashboard 4.26.3 → 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 (151) hide show
  1. package/CONFIG.md +219 -0
  2. package/README.md +60 -20
  3. package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcdashboard.js +61559 -58048
  6. package/examples/__data__/data-2.json +6 -0
  7. package/examples/__data__/data.json +6 -0
  8. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  9. package/examples/dashboard-missing-datasets-multi.json +174 -0
  10. package/examples/dashboard-missing-datasets-single.json +121 -0
  11. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  12. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  13. package/examples/dashboard-stale-dataset-keys.json +181 -0
  14. package/examples/dashboard-tiered-filter-regression.json +190 -0
  15. package/examples/legend-issue.json +1 -1
  16. package/examples/minimal-example.json +34 -0
  17. package/examples/private/cfa-dashboard.json +651 -0
  18. package/examples/private/data-bite-wrap.json +6936 -0
  19. package/examples/private/dengue.json +4640 -0
  20. package/examples/private/link_to_file.json +16662 -0
  21. package/examples/private/multi-dash-fix.json +16963 -0
  22. package/examples/private/versions.json +41612 -0
  23. package/examples/sankey.json +3 -3
  24. package/examples/test-api-filter-reset.json +4 -4
  25. package/examples/tp5-test.json +86 -4
  26. package/examples/us-map-filter-example.json +1074 -0
  27. package/package.json +9 -9
  28. package/src/CdcDashboard.tsx +6 -2
  29. package/src/CdcDashboardComponent.tsx +179 -88
  30. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  31. package/src/DashboardCopyPasteContext.tsx +48 -0
  32. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  33. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  34. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  35. package/src/_stories/Dashboard.smoke.stories.tsx +33 -0
  36. package/src/_stories/Dashboard.stories.tsx +337 -2
  37. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  38. package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
  39. package/src/_stories/_mock/tp5-test.json +86 -5
  40. package/src/components/Column.test.tsx +176 -0
  41. package/src/components/Column.tsx +214 -13
  42. package/src/components/DashboardConditionModal.test.tsx +420 -0
  43. package/src/components/DashboardConditionModal.tsx +367 -0
  44. package/src/components/DashboardConditionSummary.tsx +59 -0
  45. package/src/components/DashboardEditors.tsx +23 -0
  46. package/src/components/DashboardFilters/DashboardFilters.test.tsx +267 -0
  47. package/src/components/DashboardFilters/DashboardFilters.tsx +193 -172
  48. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  49. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +46 -6
  50. package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
  51. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
  52. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +304 -0
  53. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +43 -36
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
  55. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  56. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  57. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  58. package/src/components/DataDesignerModal.tsx +2 -1
  59. package/src/components/ExpandCollapseButtons.tsx +6 -4
  60. package/src/components/Grid.tsx +12 -7
  61. package/src/components/Header/Header.tsx +36 -17
  62. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
  63. package/src/components/Row.test.tsx +228 -0
  64. package/src/components/Row.tsx +104 -28
  65. package/src/components/VisualizationRow.test.tsx +396 -0
  66. package/src/components/VisualizationRow.tsx +177 -51
  67. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  68. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  69. package/src/components/Widget/Widget.test.tsx +218 -0
  70. package/src/components/Widget/Widget.tsx +123 -20
  71. package/src/components/Widget/widget.styles.css +58 -14
  72. package/src/components/dashboard-condition-modal.css +76 -0
  73. package/src/components/dashboard-condition-summary.css +87 -0
  74. package/src/data/initial-state.js +1 -0
  75. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  76. package/src/helpers/addVisualization.ts +17 -4
  77. package/src/helpers/cloneDashboardWidget.ts +127 -0
  78. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  79. package/src/helpers/dashboardConditionUi.ts +47 -0
  80. package/src/helpers/dashboardConditions.ts +200 -0
  81. package/src/helpers/dashboardFilterTargets.ts +156 -0
  82. package/src/helpers/filterData.ts +4 -9
  83. package/src/helpers/filterVisibility.ts +20 -0
  84. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  85. package/src/helpers/getFilteredData.ts +18 -5
  86. package/src/helpers/getUpdateConfig.ts +43 -12
  87. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  88. package/src/helpers/iconHash.tsx +9 -3
  89. package/src/helpers/mapDataToConfig.ts +31 -29
  90. package/src/helpers/reloadURLHelpers.ts +25 -5
  91. package/src/helpers/removeDashboardFilter.ts +33 -33
  92. package/src/helpers/tests/addVisualization.test.ts +53 -9
  93. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  94. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  95. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  96. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  97. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  98. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  99. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  100. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  101. package/src/index.tsx +6 -3
  102. package/src/scss/grid.scss +281 -22
  103. package/src/scss/main.scss +215 -64
  104. package/src/store/dashboard.actions.ts +17 -4
  105. package/src/store/dashboard.reducer.test.ts +538 -0
  106. package/src/store/dashboard.reducer.ts +136 -22
  107. package/src/test/CdcDashboard.test.jsx +24 -0
  108. package/src/test/CdcDashboard.test.tsx +148 -0
  109. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  110. package/src/types/ConfigRow.ts +15 -0
  111. package/src/types/DashboardFilters.ts +4 -0
  112. package/src/types/SharedFilter.ts +2 -0
  113. package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
  114. package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
  115. package/examples/DEV-6574.json +0 -2224
  116. package/examples/api-dashboard-data.json +0 -272
  117. package/examples/api-dashboard-years.json +0 -11
  118. package/examples/api-geographies-data.json +0 -11
  119. package/examples/chart-data.json +0 -5409
  120. package/examples/custom/css/respiratory.css +0 -236
  121. package/examples/custom/js/respiratory.js +0 -242
  122. package/examples/default-data.json +0 -368
  123. package/examples/default-filter-control.json +0 -209
  124. package/examples/default-multi-dataset-shared-filter.json +0 -1729
  125. package/examples/default-multi-dataset.json +0 -506
  126. package/examples/ed-visits-county-file.json +0 -402
  127. package/examples/filters/Alabama.json +0 -72
  128. package/examples/filters/Alaska.json +0 -1737
  129. package/examples/filters/Arkansas.json +0 -4713
  130. package/examples/filters/California.json +0 -212
  131. package/examples/filters/Colorado.json +0 -1500
  132. package/examples/filters/Connecticut.json +0 -559
  133. package/examples/filters/Delaware.json +0 -63
  134. package/examples/filters/DistrictofColumbia.json +0 -63
  135. package/examples/filters/Florida.json +0 -4217
  136. package/examples/filters/States.json +0 -146
  137. package/examples/state-level.json +0 -90136
  138. package/examples/state-points.json +0 -10474
  139. package/examples/temp-example-data.json +0 -130
  140. package/examples/test-dashboard-simple.json +0 -503
  141. package/examples/test-example.json +0 -752
  142. package/examples/test-file.json +0 -147
  143. package/examples/test.json +0 -752
  144. package/examples/testing.json +0 -94456
  145. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  146. /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
  147. /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
  148. /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
  149. /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
  150. /package/examples/api-test/{years.json → __data__/years.json} +0 -0
  151. /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
@@ -0,0 +1,304 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import FilterEditor from './FilterEditor'
4
+
5
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
6
+ default: props => <span data-testid='mock-icon' {...props} />
7
+ }))
8
+
9
+ const baseConfig = {
10
+ dashboard: {
11
+ sharedFilters: []
12
+ },
13
+ datasets: {
14
+ 'nested-data.json': {
15
+ data: [
16
+ { region: 'North', year: '2023', quarter: 'Q1' },
17
+ { region: 'North', year: '2023', quarter: 'Q2' }
18
+ ]
19
+ }
20
+ },
21
+ rows: [
22
+ {
23
+ columns: [
24
+ {
25
+ width: 12,
26
+ conditionalWidgets: [
27
+ {
28
+ widget: 'viz-1',
29
+ dashboardCondition: {
30
+ id: 'row-1-col-1-condition',
31
+ datasetKey: 'nested-data.json',
32
+ operator: 'hasData'
33
+ }
34
+ }
35
+ ]
36
+ }
37
+ ],
38
+ dashboardCondition: {
39
+ id: 'row-1-condition',
40
+ datasetKey: 'nested-data.json',
41
+ operator: 'hasData'
42
+ },
43
+ expandCollapseAllButtons: false
44
+ },
45
+ {
46
+ columns: [
47
+ {
48
+ width: 12,
49
+ widget: 'viz-2',
50
+ dashboardCondition: {
51
+ id: 'row-2-col-1-condition',
52
+ datasetKey: 'nested-data.json',
53
+ operator: 'hasData'
54
+ }
55
+ }
56
+ ],
57
+ dashboardCondition: {
58
+ id: 'row-2-condition',
59
+ datasetKey: 'nested-data.json',
60
+ operator: 'hasData'
61
+ },
62
+ expandCollapseAllButtons: false,
63
+ toggle: true
64
+ }
65
+ ],
66
+ visualizations: {
67
+ 'viz-1': {
68
+ uid: 'viz-1',
69
+ type: 'markup-include',
70
+ contentEditor: {
71
+ title: 'First Markup'
72
+ }
73
+ },
74
+ 'viz-2': {
75
+ uid: 'viz-2',
76
+ type: 'markup-include',
77
+ contentEditor: {
78
+ title: 'Toggle Markup'
79
+ }
80
+ }
81
+ }
82
+ } as any
83
+
84
+ const createNestedFilter = (type: 'datafilter' | 'urlfilter') =>
85
+ ({
86
+ key: 'Year and Quarter',
87
+ type,
88
+ filterStyle: 'nested-dropdown',
89
+ showDropdown: true,
90
+ values: ['2023', '2024'],
91
+ columnName: 'year',
92
+ id: 0,
93
+ parents: [],
94
+ order: 'asc',
95
+ subGrouping: {
96
+ columnName: 'quarter',
97
+ valuesLookup: {
98
+ '2023': { values: ['Q1', 'Q2'] },
99
+ '2024': { values: ['Q3', 'Q4'] }
100
+ }
101
+ },
102
+ ...(type === 'urlfilter'
103
+ ? {
104
+ apiFilter: {
105
+ apiEndpoint: '/api/nested-options',
106
+ valueSelector: 'year',
107
+ subgroupValueSelector: 'quarter'
108
+ }
109
+ }
110
+ : {})
111
+ } as any)
112
+
113
+ describe('FilterEditor API filter subgroup text selector', () => {
114
+ it('displays subgroupTextSelector value from apiFilter', () => {
115
+ const filter = {
116
+ ...createNestedFilter('urlfilter'),
117
+ apiFilter: {
118
+ apiEndpoint: '/api/nested-options',
119
+ valueSelector: 'year',
120
+ subgroupValueSelector: 'quarter',
121
+ subgroupTextSelector: 'quarterName'
122
+ }
123
+ } as any
124
+
125
+ render(
126
+ <FilterEditor
127
+ config={{
128
+ ...baseConfig,
129
+ dashboard: { sharedFilters: [filter] }
130
+ }}
131
+ filter={filter}
132
+ filterIndex={0}
133
+ onNestedDragAreaHover={vi.fn()}
134
+ toggleNestedQueryParameters={vi.fn()}
135
+ updateFilterProp={vi.fn()}
136
+ />
137
+ )
138
+
139
+ expect(screen.getByDisplayValue('quarterName')).toBeInTheDocument()
140
+ })
141
+
142
+ it('shows empty string when subgroupTextSelector is absent', () => {
143
+ const filter = createNestedFilter('urlfilter') as any
144
+
145
+ render(
146
+ <FilterEditor
147
+ config={{
148
+ ...baseConfig,
149
+ dashboard: { sharedFilters: [filter] }
150
+ }}
151
+ filter={filter}
152
+ filterIndex={0}
153
+ onNestedDragAreaHover={vi.fn()}
154
+ toggleNestedQueryParameters={vi.fn()}
155
+ updateFilterProp={vi.fn()}
156
+ />
157
+ )
158
+
159
+ const inputs = screen.getAllByRole('textbox')
160
+ const subgroupTextInput = inputs.find(el =>
161
+ el.closest('label')?.textContent?.includes('Subgroup Display Text Selector')
162
+ )
163
+ expect(subgroupTextInput).toHaveValue('')
164
+ })
165
+ })
166
+
167
+ describe('FilterEditor nested dropdown display toggle', () => {
168
+ it.each([
169
+ ['data-backed nested filters', createNestedFilter('datafilter')],
170
+ ['api-backed nested filters', createNestedFilter('urlfilter')]
171
+ ])('renders the checkbox below Create query parameters for %s', (_label, filter) => {
172
+ const updateFilterProp = vi.fn()
173
+
174
+ render(
175
+ <FilterEditor
176
+ config={{
177
+ ...baseConfig,
178
+ dashboard: { sharedFilters: [filter] }
179
+ }}
180
+ filter={filter}
181
+ filterIndex={0}
182
+ onNestedDragAreaHover={vi.fn()}
183
+ toggleNestedQueryParameters={vi.fn()}
184
+ updateFilterProp={updateFilterProp}
185
+ />
186
+ )
187
+
188
+ const queryParameters = screen.getByLabelText('Create query parameters')
189
+ const displaySubgroupingOnly = screen.getByLabelText('Display subgrouping only')
190
+
191
+ expect(displaySubgroupingOnly).not.toBeChecked()
192
+
193
+ const queryParametersLabel = queryParameters.closest('label')
194
+ const displaySubgroupingOnlyLabel = displaySubgroupingOnly.closest('label')
195
+ const isBelowQueryParameters = !!(
196
+ queryParametersLabel &&
197
+ displaySubgroupingOnlyLabel &&
198
+ queryParametersLabel.compareDocumentPosition(displaySubgroupingOnlyLabel) & Node.DOCUMENT_POSITION_FOLLOWING
199
+ )
200
+
201
+ expect(isBelowQueryParameters).toBe(true)
202
+
203
+ fireEvent.click(displaySubgroupingOnly)
204
+
205
+ expect(updateFilterProp).toHaveBeenCalledWith('displaySubgroupingOnly', true)
206
+ })
207
+
208
+ it.each([
209
+ [
210
+ 'data-backed non-nested filters',
211
+ {
212
+ ...createNestedFilter('datafilter'),
213
+ filterStyle: 'dropdown'
214
+ }
215
+ ],
216
+ [
217
+ 'api-backed non-nested filters',
218
+ {
219
+ ...createNestedFilter('urlfilter'),
220
+ filterStyle: 'dropdown'
221
+ }
222
+ ]
223
+ ])('does not render the checkbox for %s', (_label, filter) => {
224
+ render(
225
+ <FilterEditor
226
+ config={{
227
+ ...baseConfig,
228
+ dashboard: { sharedFilters: [filter] }
229
+ }}
230
+ filter={filter}
231
+ filterIndex={0}
232
+ onNestedDragAreaHover={vi.fn()}
233
+ toggleNestedQueryParameters={vi.fn()}
234
+ updateFilterProp={vi.fn()}
235
+ />
236
+ )
237
+
238
+ expect(screen.queryByLabelText('Display subgrouping only')).not.toBeInTheDocument()
239
+ })
240
+
241
+ it('uses row targets for row conditions and does not expose dashboard condition ids in Used By options', () => {
242
+ render(
243
+ <FilterEditor
244
+ config={{
245
+ ...baseConfig,
246
+ dashboard: {
247
+ sharedFilters: [
248
+ {
249
+ ...createNestedFilter('datafilter'),
250
+ filterStyle: 'dropdown'
251
+ }
252
+ ]
253
+ }
254
+ }}
255
+ filter={{
256
+ ...createNestedFilter('datafilter'),
257
+ filterStyle: 'dropdown'
258
+ }}
259
+ filterIndex={0}
260
+ onNestedDragAreaHover={vi.fn()}
261
+ toggleNestedQueryParameters={vi.fn()}
262
+ updateFilterProp={vi.fn()}
263
+ />
264
+ )
265
+
266
+ const expandButtons = screen.getAllByLabelText('Expand')
267
+ fireEvent.click(expandButtons[0])
268
+
269
+ expect(screen.getByText('Row 1')).toBeInTheDocument()
270
+ expect(screen.queryByText('Row 1 Dashboard Condition')).not.toBeInTheDocument()
271
+ expect(screen.queryByText('Row 1 Column 1 Component 1 Dashboard Condition')).not.toBeInTheDocument()
272
+ expect(screen.queryByText('Row 2 Dashboard Condition')).not.toBeInTheDocument()
273
+ expect(screen.queryByText('Row 2 Column 1 Dashboard Condition')).not.toBeInTheDocument()
274
+ })
275
+
276
+ it('updates dashboard shared filter note text', async () => {
277
+ const filter = {
278
+ ...createNestedFilter('datafilter'),
279
+ filterStyle: 'dropdown',
280
+ note: 'Existing note'
281
+ }
282
+ const updateFilterProp = vi.fn()
283
+
284
+ render(
285
+ <FilterEditor
286
+ config={{
287
+ ...baseConfig,
288
+ dashboard: { sharedFilters: [filter] }
289
+ }}
290
+ filter={filter}
291
+ filterIndex={0}
292
+ onNestedDragAreaHover={vi.fn()}
293
+ toggleNestedQueryParameters={vi.fn()}
294
+ updateFilterProp={updateFilterProp}
295
+ />
296
+ )
297
+
298
+ fireEvent.change(screen.getByLabelText('Note'), { target: { value: 'Helpful note' } })
299
+
300
+ await waitFor(() => {
301
+ expect(updateFilterProp).toHaveBeenCalledWith('note', 'Helpful note')
302
+ })
303
+ })
304
+ })
@@ -1,7 +1,7 @@
1
- import { getVizRowColumnLocator } from '../../../../helpers/getVizRowColumnLocator'
2
1
  import { Select, TextField } from '@cdc/core/components/EditorPanel/Inputs'
3
2
  import DataTransform from '@cdc/core/helpers/DataTransform'
4
3
  import { useEffect, useMemo, useState } from 'react'
4
+ import { getSharedFilterTargetOptions } from '../../../../helpers/dashboardFilterTargets'
5
5
  import { SharedFilter } from '../../../../types/SharedFilter'
6
6
 
7
7
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
@@ -10,7 +10,6 @@ import Icon from '@cdc/core/components/ui/Icon'
10
10
  import MultiSelect from '@cdc/core/components/MultiSelect'
11
11
  import Loading from '@cdc/core/components/Loading'
12
12
  import { DashboardConfig } from '../../../../types/DashboardConfig'
13
- import { Visualization } from '@cdc/core/types/Visualization'
14
13
  import { hasDashboardApplyBehavior } from '../../../../helpers/hasDashboardApplyBehavior'
15
14
  import APIModal from './APIModal'
16
15
  import NestedDropDownDashboard from './NestedDropDownDashboard'
@@ -19,6 +18,7 @@ import { filterOrderOptions } from '@cdc/core/helpers/filterOrderOptions'
19
18
  import FilterOrder from '@cdc/core/components/EditorPanel/VizFilterEditor/components/FilterOrder'
20
19
  import { useGlobalContext } from '@cdc/core/components/GlobalContext'
21
20
  import Modal from '@cdc/core/components/ui/Modal'
21
+ import Button from '@cdc/core/components/elements/Button'
22
22
 
23
23
  type FilterEditorProps = {
24
24
  config: DashboardConfig
@@ -51,43 +51,18 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
51
51
  .filter(({ key }) => key !== filter.key)
52
52
  .map(({ key }) => key)
53
53
 
54
- const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
55
-
56
54
  const getVizTitle = (viz, vizKey) => {
57
55
  let vizName = viz.general?.title || viz.title || vizKey
58
56
  if (viz.visualizationType === 'markup-include') {
59
- vizName = viz.contentEditor.title || vizKey
57
+ vizName = viz.contentEditor?.title || vizKey
60
58
  }
61
59
  return vizName
62
60
  }
63
61
 
64
62
  const [usedByNameLookup, usedByOptions] = useMemo(() => {
65
- const nameLookup = {}
66
- const vizOptions = Object.keys(config.visualizations).filter(vizKey => {
67
- const vizLookup = vizRowColumnLocator[vizKey]
68
- if (!vizLookup) return false
69
- const viz = config.visualizations[vizKey] as Visualization
70
- if (viz.type === 'dashboardFilters') return false
71
- const vizName = getVizTitle(viz, vizKey)
72
-
73
- nameLookup[vizKey] = vizName
74
- const usesSharedFilter = viz.usesSharedFilter
75
- const rowIndex = vizLookup.row
76
- const dataConfiguredOnRow = config.rows[rowIndex].dataKey
77
- return filter.setBy !== vizKey && !usesSharedFilter && !dataConfiguredOnRow
78
- })
79
- const rowOptions: number[] = []
80
-
81
- config.rows.forEach((row, rowIndex) => {
82
- if (!!row.dataKey) {
83
- nameLookup[rowIndex] = `Row ${rowIndex + 1}`
84
- rowOptions.push(rowIndex)
85
- }
86
- })
87
-
88
- const rowsNotSelected = rowOptions.filter(row => !filter.usedBy || filter.usedBy.indexOf(row.toString()) === -1)
89
- return [nameLookup, [...vizOptions, ...rowsNotSelected]]
90
- }, [config.visualizations, filter.usedBy, filter.setBy, vizRowColumnLocator])
63
+ const { nameLookup, options } = getSharedFilterTargetOptions(config, filter)
64
+ return [nameLookup, options]
65
+ }, [config, filter])
91
66
 
92
67
  const useParameters = useMemo(() => {
93
68
  if (filter.subGrouping) return !!(filter.setByQueryParameter && filter.subGrouping?.setByQueryParameter)
@@ -223,11 +198,20 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
223
198
 
224
199
  <TextField
225
200
  label='Label'
201
+ fieldName='key'
226
202
  value={filter.key}
227
203
  updateField={(_section, _subSection, _key, value) => {
228
204
  updateLabel(value)
229
205
  }}
230
206
  />
207
+ <TextField
208
+ type='textarea'
209
+ className='filter-editor__compact-textarea'
210
+ label='Note'
211
+ fieldName='note'
212
+ value={filter.note || ''}
213
+ updateField={(_section, _subSection, _key, value) => updateFilterProp('note', value)}
214
+ />
231
215
  {filter.filterStyle === FILTER_STYLE.multiSelect && (
232
216
  <TextField
233
217
  label='Select Limit'
@@ -292,7 +276,7 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
292
276
  {filter.filterBy === 'Query String' && filter.usedBy && filter.usedBy.length > 0 && (
293
277
  <div className='bg-info-subtle p-2 my-2' style={{ fontSize: '0.9em' }}>
294
278
  <Icon display='info' style={{ marginRight: '0.5rem' }} />
295
- Will apply to datasets used by selected widgets
279
+ Will apply to datasets used by selected targets
296
280
  </div>
297
281
  )}
298
282
  {filter.filterBy === 'File Name' && (
@@ -405,7 +389,7 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
405
389
  </label>
406
390
  <label>
407
391
  <span>Subgroup Display Text Selector: </span>
408
- <input value={filter?.apifilter?.subgroupTextSelector || ''} disabled />
392
+ <input value={filter?.apiFilter?.subgroupTextSelector || ''} disabled />
409
393
  <Tooltip style={{ textTransform: 'none' }}>
410
394
  <Tooltip.Target>
411
395
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
@@ -419,12 +403,13 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
419
403
  </div>
420
404
  )}
421
405
 
422
- <button
406
+ <Button
407
+ variant='primary'
408
+ className='mt-2'
423
409
  onClick={() => handleEditAPIValues(filter, isNestedDropdown, updateAPIFilter)}
424
- className='btn btn-primary mt-2'
425
410
  >
426
411
  Edit API Values
427
- </button>
412
+ </Button>
428
413
  </div>
429
414
 
430
415
  <label>
@@ -451,6 +436,18 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
451
436
  </span>
452
437
  </label>
453
438
 
439
+ {isNestedDropdown && (
440
+ <label>
441
+ <input
442
+ type='checkbox'
443
+ checked={!!filter.displaySubgroupingOnly}
444
+ aria-label='Display subgrouping only'
445
+ onChange={e => updateFilterProp('displaySubgroupingOnly', e.target.checked)}
446
+ />
447
+ <span> Display subgrouping only</span>
448
+ </label>
449
+ )}
450
+
454
451
  {!!parentFilters.length && (
455
452
  <label>
456
453
  <span className='edit-label column-heading mt-1'>Parent Filter(s): </span>
@@ -596,6 +593,16 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
596
593
  </Tooltip>
597
594
  </span>
598
595
  </label>
596
+
597
+ <label>
598
+ <input
599
+ type='checkbox'
600
+ checked={!!filter.displaySubgroupingOnly}
601
+ aria-label='Display subgrouping only'
602
+ onChange={e => updateFilterProp('displaySubgroupingOnly', e.target.checked)}
603
+ />
604
+ <span> Display subgrouping only</span>
605
+ </label>
599
606
  </>
600
607
  )}
601
608
  <Select
@@ -242,7 +242,7 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
242
242
  />
243
243
  )}
244
244
 
245
- {/* Default Value for Sub Group */}
245
+ {/* Default Value for Subgroup */}
246
246
  {subGrouping?.columnName && (filter.defaultValue || filter.active) && subGrouping.valuesLookup && (
247
247
  <Select
248
248
  value={subGrouping.defaultValue}
@@ -255,7 +255,7 @@ const NestedDropDownDashboard: React.FC<NestedDropDownEditorDashboardProps> = ({
255
255
  const newSubGrouping = { ...subGrouping, defaultValue: value }
256
256
  updateFilterProp('subGrouping', newSubGrouping)
257
257
  }}
258
- label={'Sub Group Default Value'}
258
+ label={'Subgroup Default Value'}
259
259
  initial={'Select'}
260
260
  />
261
261
  )}
@@ -0,0 +1,142 @@
1
+ import React from 'react'
2
+ import { render } from '@testing-library/react'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { DashboardContext, DashboardDispatchContext, initialState } from '../../DashboardContext'
5
+ import DashboardFiltersWrapper from './DashboardFiltersWrapper'
6
+
7
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
8
+ default: props => <span data-testid='mock-icon' {...props} />
9
+ }))
10
+
11
+ const createSharedFilter = (overrides = {}) =>
12
+ ({
13
+ key: 'Year',
14
+ type: 'datafilter',
15
+ filterStyle: 'dropdown',
16
+ showDropdown: true,
17
+ values: ['2023', '2024'],
18
+ active: '2023',
19
+ columnName: 'year',
20
+ parents: [],
21
+ ...overrides
22
+ } as any)
23
+
24
+ const renderWrapper = ({
25
+ grayBackground,
26
+ sharedFilters = [createSharedFilter()],
27
+ sharedFilterIndexes = [0],
28
+ isEditor = false
29
+ }: {
30
+ grayBackground?: boolean
31
+ sharedFilters?: any[]
32
+ sharedFilterIndexes?: any[]
33
+ isEditor?: boolean
34
+ } = {}) => {
35
+ const visualizationConfig = {
36
+ uid: 'dashboardFilters1',
37
+ type: 'dashboardFilters',
38
+ visualizationType: 'dashboardFilters',
39
+ filterBehavior: 'Filter Change',
40
+ filterIntro: 'Choose a <strong>year</strong> to update the dashboard.',
41
+ sharedFilterIndexes,
42
+ visual: grayBackground === undefined ? undefined : { grayBackground }
43
+ } as any
44
+
45
+ return render(
46
+ <DashboardContext.Provider
47
+ value={{
48
+ ...initialState,
49
+ config: {
50
+ type: 'dashboard',
51
+ dashboard: { sharedFilters },
52
+ datasets: {},
53
+ rows: [],
54
+ visualizations: {
55
+ dashboardFilters1: visualizationConfig
56
+ }
57
+ } as any,
58
+ data: {},
59
+ outerContainerRef: vi.fn(),
60
+ setParentConfig: vi.fn(),
61
+ isDebug: false,
62
+ isEditor: false,
63
+ reloadURLData: vi.fn(),
64
+ loadAPIFilters: vi.fn(),
65
+ setAPIFilterDropdowns: vi.fn(),
66
+ setAPILoading: vi.fn()
67
+ }}
68
+ >
69
+ <DashboardDispatchContext.Provider value={vi.fn()}>
70
+ <DashboardFiltersWrapper
71
+ apiFilterDropdowns={{}}
72
+ visualizationConfig={visualizationConfig}
73
+ setConfig={vi.fn()}
74
+ currentViewport={'lg' as any}
75
+ isEditor={isEditor}
76
+ interactionLabel='dashboard-test'
77
+ />
78
+ </DashboardDispatchContext.Provider>
79
+ </DashboardContext.Provider>
80
+ )
81
+ }
82
+
83
+ describe('DashboardFiltersWrapper visual styles', () => {
84
+ it('wraps filters in the dashboard filters callout when grey background is enabled', () => {
85
+ const { container } = renderWrapper({ grayBackground: true })
86
+
87
+ expect(container.querySelector('.cdc-callout.cdc-callout--dashboard-filters')).toBeInTheDocument()
88
+ })
89
+
90
+ it.each([false, undefined])('keeps the existing unwrapped layout when grayBackground is %s', value => {
91
+ const { container } = renderWrapper({ grayBackground: value })
92
+
93
+ expect(container.querySelector('.cdc-callout.cdc-callout--dashboard-filters')).not.toBeInTheDocument()
94
+ })
95
+
96
+ it('renders filter intro text above dashboard filter controls', () => {
97
+ const { container } = renderWrapper()
98
+ const intro = container.querySelector('.filters-section__intro-text')
99
+
100
+ expect(intro).toBeInTheDocument()
101
+ expect(intro).toHaveTextContent('Choose a year to update the dashboard.')
102
+ expect(intro?.querySelector('strong')).toHaveTextContent('year')
103
+ })
104
+
105
+ it('renders no dashboard filters container when every referenced filter is hidden', () => {
106
+ const { container } = renderWrapper({ sharedFilters: [createSharedFilter({ showDropdown: false })] })
107
+
108
+ expect(container.querySelector('.cove-dashboard-filters-container')).not.toBeInTheDocument()
109
+ })
110
+
111
+ it('renders no dashboard filters container when sharedFilterIndexes is empty', () => {
112
+ const { container } = renderWrapper({ sharedFilterIndexes: [] })
113
+
114
+ expect(container.querySelector('.cove-dashboard-filters-container')).not.toBeInTheDocument()
115
+ })
116
+
117
+ it('renders no dashboard filters container when sharedFilterIndexes only references missing filters', () => {
118
+ const { container } = renderWrapper({ sharedFilterIndexes: [4] })
119
+
120
+ expect(container.querySelector('.cove-dashboard-filters-container')).not.toBeInTheDocument()
121
+ })
122
+
123
+ it('keeps the editor sidebar available without rendering an empty runtime filters container', () => {
124
+ const { container, getByText } = renderWrapper({
125
+ sharedFilters: [createSharedFilter({ showDropdown: false })],
126
+ isEditor: true
127
+ })
128
+
129
+ expect(getByText('Configure Dashboard Filters')).toBeInTheDocument()
130
+ expect(container.querySelector('.cove-dashboard-filters-container')).not.toBeInTheDocument()
131
+ })
132
+
133
+ it.each([
134
+ ['urlfilter', createSharedFilter({ type: 'urlfilter', showDropdown: false })],
135
+ ['nestedDropdown', createSharedFilter({ filterStyle: 'nested-dropdown', showDropdown: false })],
136
+ ['tabSimple', createSharedFilter({ filterStyle: 'tab-simple', showDropdown: false })]
137
+ ])('keeps %s dashboard filters visible under dashboard-specific rules', (_label, filter) => {
138
+ const { container } = renderWrapper({ sharedFilters: [filter] })
139
+
140
+ expect(container.querySelector('.cove-dashboard-filters-container')).toBeInTheDocument()
141
+ })
142
+ })