@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
package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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?.
|
|
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
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
?.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
.
|
|
2
|
-
:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
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
|
})
|
package/src/components/Grid.tsx
CHANGED
|
@@ -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
|
|
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
|