@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
@@ -1,4 +1,4 @@
1
- import { fireEvent, render, screen } from '@testing-library/react'
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
  import FilterEditor from './FilterEditor'
4
4
 
@@ -18,8 +18,67 @@ const baseConfig = {
18
18
  ]
19
19
  }
20
20
  },
21
- rows: [],
22
- visualizations: {}
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
+ }
23
82
  } as any
24
83
 
25
84
  const createNestedFilter = (type: 'datafilter' | 'urlfilter') =>
@@ -51,6 +110,60 @@ const createNestedFilter = (type: 'datafilter' | 'urlfilter') =>
51
110
  : {})
52
111
  } as any)
53
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
+
54
167
  describe('FilterEditor nested dropdown display toggle', () => {
55
168
  it.each([
56
169
  ['data-backed nested filters', createNestedFilter('datafilter')],
@@ -124,4 +237,68 @@ describe('FilterEditor nested dropdown display toggle', () => {
124
237
 
125
238
  expect(screen.queryByLabelText('Display subgrouping only')).not.toBeInTheDocument()
126
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
+ })
127
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'
@@ -52,8 +51,6 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
52
51
  .filter(({ key }) => key !== filter.key)
53
52
  .map(({ key }) => key)
54
53
 
55
- const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
56
-
57
54
  const getVizTitle = (viz, vizKey) => {
58
55
  let vizName = viz.general?.title || viz.title || vizKey
59
56
  if (viz.visualizationType === 'markup-include') {
@@ -63,32 +60,9 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
63
60
  }
64
61
 
65
62
  const [usedByNameLookup, usedByOptions] = useMemo(() => {
66
- const nameLookup = {}
67
- const vizOptions = Object.keys(config.visualizations).filter(vizKey => {
68
- const vizLookup = vizRowColumnLocator[vizKey]
69
- if (!vizLookup) return false
70
- const viz = config.visualizations[vizKey] as Visualization
71
- if (viz.type === 'dashboardFilters') return false
72
- const vizName = getVizTitle(viz, vizKey)
73
-
74
- nameLookup[vizKey] = vizName
75
- const usesSharedFilter = viz.usesSharedFilter
76
- const rowIndex = vizLookup.row
77
- const dataConfiguredOnRow = config.rows[rowIndex].dataKey
78
- return filter.setBy !== vizKey && !usesSharedFilter && !dataConfiguredOnRow
79
- })
80
- const rowOptions: number[] = []
81
-
82
- config.rows.forEach((row, rowIndex) => {
83
- if (!!row.dataKey) {
84
- nameLookup[rowIndex] = `Row ${rowIndex + 1}`
85
- rowOptions.push(rowIndex)
86
- }
87
- })
88
-
89
- const rowsNotSelected = rowOptions.filter(row => !filter.usedBy || filter.usedBy.indexOf(row.toString()) === -1)
90
- return [nameLookup, [...vizOptions, ...rowsNotSelected]]
91
- }, [config.visualizations, filter.usedBy, filter.setBy, vizRowColumnLocator])
63
+ const { nameLookup, options } = getSharedFilterTargetOptions(config, filter)
64
+ return [nameLookup, options]
65
+ }, [config, filter])
92
66
 
93
67
  const useParameters = useMemo(() => {
94
68
  if (filter.subGrouping) return !!(filter.setByQueryParameter && filter.subGrouping?.setByQueryParameter)
@@ -224,11 +198,20 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
224
198
 
225
199
  <TextField
226
200
  label='Label'
201
+ fieldName='key'
227
202
  value={filter.key}
228
203
  updateField={(_section, _subSection, _key, value) => {
229
204
  updateLabel(value)
230
205
  }}
231
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
+ />
232
215
  {filter.filterStyle === FILTER_STYLE.multiSelect && (
233
216
  <TextField
234
217
  label='Select Limit'
@@ -293,7 +276,7 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
293
276
  {filter.filterBy === 'Query String' && filter.usedBy && filter.usedBy.length > 0 && (
294
277
  <div className='bg-info-subtle p-2 my-2' style={{ fontSize: '0.9em' }}>
295
278
  <Icon display='info' style={{ marginRight: '0.5rem' }} />
296
- Will apply to datasets used by selected widgets
279
+ Will apply to datasets used by selected targets
297
280
  </div>
298
281
  )}
299
282
  {filter.filterBy === 'File Name' && (
@@ -406,7 +389,7 @@ const FilterEditor: React.FC<FilterEditorProps> = ({
406
389
  </label>
407
390
  <label>
408
391
  <span>Subgroup Display Text Selector: </span>
409
- <input value={filter?.apifilter?.subgroupTextSelector || ''} disabled />
392
+ <input value={filter?.apiFilter?.subgroupTextSelector || ''} disabled />
410
393
  <Tooltip style={{ textTransform: 'none' }}>
411
394
  <Tooltip.Target>
412
395
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
@@ -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
+ })
@@ -14,10 +14,10 @@ import { hasDashboardApplyBehavior } from '../../helpers/hasDashboardApplyBehavi
14
14
  import * as apiFilterHelpers from '../../helpers/apiFilterHelpers'
15
15
  import * as filterResetHelpers from '../../helpers/filterResetHelpers'
16
16
  import { applyQueuedActive } from '@cdc/core/components/Filters/helpers/applyQueuedActive'
17
- import './dashboardfilter.styles.css'
18
17
  import { updateChildFilters } from '../../helpers/updateChildFilters'
19
18
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
20
19
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
20
+ import { hasVisibleDashboardFiltersForIndexes } from '../../helpers/filterVisibility'
21
21
 
22
22
  type SubOptions = { subOptions?: Record<'value' | 'text', string>[] }
23
23
 
@@ -193,7 +193,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
193
193
  }
194
194
  }
195
195
  const newFilteredData = getFilteredData(clonedState)
196
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
196
+ dispatch({ type: 'SET_FILTERED_DATA', payload: { filteredData: newFilteredData } })
197
197
 
198
198
  publishAnalyticsEvent({
199
199
  vizType: dashboardConfig.type,
@@ -260,7 +260,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
260
260
  newSharedFilters[index].queuedActive = value
261
261
 
262
262
  // Don't clear data immediately - keep existing data until new data loads
263
- // Only update the filter dropdowns and prepare for reload
263
+ // Only update the dashboard filters and prepare for reload
264
264
  setAPIFilterDropdowns(loadingFilterMemo)
265
265
  loadAPIFilters(newSharedFilters, loadingFilterMemo, undefined, undefined, isStale)
266
266
  }
@@ -280,7 +280,7 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
280
280
  }
281
281
  }
282
282
  const newFilteredData = getFilteredData(clonedState)
283
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
283
+ dispatch({ type: 'SET_FILTERED_DATA', payload: { filteredData: newFilteredData } })
284
284
  dispatch({ type: 'SET_SHARED_FILTERS', payload: updatedFilters })
285
285
  }
286
286
  }
@@ -294,13 +294,28 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
294
294
  })
295
295
  }
296
296
 
297
- // if all of the filters are hidden filters don't display the VisualizationWrapper
298
- const filters = visualizationConfig?.sharedFilterIndexes
299
- ?.map(Number)
300
- ?.map(filterIndex => dashboardConfig.dashboard.sharedFilters[filterIndex])
301
-
302
- const displayNone = filters?.length ? filters.every(filter => filter.showDropdown === false) : false
303
- if (displayNone && !isEditor) return <></>
297
+ const hasVisibleFilterControls = hasVisibleDashboardFiltersForIndexes(
298
+ dashboardConfig.dashboard.sharedFilters,
299
+ visualizationConfig?.sharedFilterIndexes
300
+ )
301
+ const filterControls = (
302
+ <Filters
303
+ show={visualizationConfig?.sharedFilterIndexes?.map(Number)}
304
+ filters={updateChildFilters(dashboardConfig.dashboard.sharedFilters, state.data) || []}
305
+ apiFilterDropdowns={apiFilterDropdowns}
306
+ handleOnChange={handleOnChange}
307
+ showSubmit={visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad}
308
+ filterIntro={visualizationConfig.filterIntro}
309
+ applyFilters={applyFilters}
310
+ applyFiltersButtonText={visualizationConfig.applyFiltersButtonText}
311
+ handleReset={
312
+ visualizationConfig.filterBehavior === FilterBehavior.Apply && (visualizationConfig.showClearButton ?? true)
313
+ ? handleReset
314
+ : undefined
315
+ }
316
+ />
317
+ )
318
+ if (!hasVisibleFilterControls && !isEditor) return <></>
304
319
  return (
305
320
  <VisualizationWrapper config={visualizationConfig} isEditor={isEditor} currentViewport={currentViewport}>
306
321
  {isEditor && (
@@ -314,28 +329,18 @@ const DashboardFiltersWrapper: React.FC<DashboardFiltersProps> = ({
314
329
  </Sidebar>
315
330
  )}
316
331
 
317
- {!displayNone && (
332
+ {hasVisibleFilterControls && (
318
333
  <Responsive isEditor={isEditor}>
319
334
  <div
320
335
  className={`${
321
336
  isEditor ? ' is-editor' : ''
322
337
  } cove-visualization__inner cove-visualization__body col-12 cove-dashboard-filters-container`}
323
338
  >
324
- <Filters
325
- show={visualizationConfig?.sharedFilterIndexes?.map(Number)}
326
- filters={updateChildFilters(dashboardConfig.dashboard.sharedFilters, state.data) || []}
327
- apiFilterDropdowns={apiFilterDropdowns}
328
- handleOnChange={handleOnChange}
329
- showSubmit={visualizationConfig.filterBehavior === FilterBehavior.Apply && !visualizationConfig.autoLoad}
330
- applyFilters={applyFilters}
331
- applyFiltersButtonText={visualizationConfig.applyFiltersButtonText}
332
- handleReset={
333
- visualizationConfig.filterBehavior === FilterBehavior.Apply &&
334
- (visualizationConfig.showClearButton ?? true)
335
- ? handleReset
336
- : undefined
337
- }
338
- />
339
+ {visualizationConfig.visual?.grayBackground ? (
340
+ <div className='cdc-callout cdc-callout--dashboard-filters'>{filterControls}</div>
341
+ ) : (
342
+ filterControls
343
+ )}
339
344
  </div>
340
345
  </Responsive>
341
346
  )}
@@ -1,27 +1,42 @@
1
- .cove-dashboard-filters-container {
2
- :is(label) {
3
- font-size: var(--filter-label-font-size);
4
- font-weight: 700;
5
- }
6
- .btn {
7
- align-self: flex-end;
8
- /* this is the height that is defined for the .form-control class in _forms.scss in bootstrap. */
9
- height: calc(1.5em + 0.75rem + 2px);
10
- }
11
- .loading-filter {
12
- position: relative;
13
- .spinner-border {
14
- height: 1.5rem;
15
- position: absolute;
16
- right: 10%;
17
- top: 55%;
18
- width: 1.5rem;
19
- }
20
- }
21
- :is(select):disabled {
22
- background-color: var(--lightestGray);
23
- & > :is(option) {
24
- color: var(--darkGray);
25
- }
26
- }
27
- }
1
+ .dashboard-filters__form {
2
+ align-items: flex-end;
3
+ display: flex;
4
+ flex-wrap: wrap;
5
+ gap: 1rem 1.5rem;
6
+ }
7
+
8
+ .dashboard-filters__field {
9
+ margin: 0;
10
+ }
11
+
12
+ .cove-dashboard-filters-container {
13
+ .cdc-callout--dashboard-filters {
14
+ --cdc-callout-background: #f4f8fa;
15
+ }
16
+
17
+ :is(label) {
18
+ font-size: var(--filter-label-font-size);
19
+ font-weight: 700;
20
+ }
21
+ .btn {
22
+ align-self: flex-end;
23
+ /* this is the height that is defined for the .form-control class in _forms.scss in bootstrap. */
24
+ height: calc(1.5em + 0.75rem + 2px);
25
+ }
26
+ .loading-filter {
27
+ position: relative;
28
+ .spinner-border {
29
+ height: 1.5rem;
30
+ position: absolute;
31
+ right: 10%;
32
+ top: 55%;
33
+ width: 1.5rem;
34
+ }
35
+ }
36
+ :is(select):disabled {
37
+ background-color: var(--lightestGray);
38
+ & > :is(option) {
39
+ color: var(--darkGray);
40
+ }
41
+ }
42
+ }
@@ -11,6 +11,7 @@ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
11
11
  import DataTransform from '@cdc/core/helpers/DataTransform'
12
12
  import { ConfigureData } from '@cdc/core/types/ConfigureData'
13
13
  import Icon from '@cdc/core/components/ui/Icon'
14
+ import { getColumnWidgetKeys } from '../helpers/dashboardColumnWidgets'
14
15
 
15
16
  type DataDesignerModalProps = {
16
17
  rowIndex: number
@@ -79,7 +80,7 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
79
80
  }
80
81
 
81
82
  const removeDatasetsFromVisualizations = () => {
82
- const columnVisualizations = config.rows[rowIndex].columns.map(column => column.widget).filter(Boolean)
83
+ const columnVisualizations = config.rows[rowIndex].columns.flatMap(column => getColumnWidgetKeys(column))
83
84
  columnVisualizations.forEach(currentVisualizationKey => {
84
85
  dispatch({ type: 'RESET_VISUALIZATION', payload: { vizKey: currentVisualizationKey } })
85
86
  })
@@ -4,6 +4,7 @@ import Button from '@cdc/core/components/elements/Button'
4
4
 
5
5
  import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
6
6
  import { ConfigRow } from '../types/ConfigRow'
7
+ import { createCoveId } from '@cdc/core/helpers/createCoveId'
7
8
 
8
9
  const Grid = () => {
9
10
  const { config } = useContext(DashboardContext)
@@ -12,18 +13,21 @@ const Grid = () => {
12
13
  const rows = config.rows
13
14
  const updateConfig = config => dispatch({ type: 'UPDATE_CONFIG', payload: [config] })
14
15
  const addRow = () => {
15
- const blankRow: Partial<ConfigRow> = { columns: [{ width: 12 }] }
16
+ const existingRowUuids = rows?.flatMap(row => (row.uuid === undefined ? [] : [row.uuid]))
17
+ const blankRow: Partial<ConfigRow> = {
18
+ columns: [{ width: 12 }],
19
+ uuid: createCoveId('row', { existingIds: existingRowUuids })
20
+ }
16
21
  updateConfig({
17
22
  ...config,
18
- rows: [...rows, blankRow],
19
- uuid: Date.now()
23
+ rows: [...rows, blankRow]
20
24
  })
21
25
  }
22
26
 
23
27
  return (
24
28
  <div className='builder-grid'>
25
29
  {(rows || []).map((row, idx) => (
26
- <Row row={row} idx={idx} uuid={row.uuid} key={idx} />
30
+ <Row row={row} idx={idx} uuid={row.uuid ?? idx} key={idx} />
27
31
  ))}
28
32
  <Button variant='primary' className='col' onClick={addRow}>
29
33
  Add Row