@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
@@ -0,0 +1,367 @@
1
+ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
2
+ import DataTransform from '@cdc/core/helpers/DataTransform'
3
+ import Button from '@cdc/core/components/elements/Button'
4
+ import Loader from '@cdc/core/components/Loader'
5
+ import Modal from '@cdc/core/components/ui/Modal'
6
+ import MultiSelect from '@cdc/core/components/MultiSelect'
7
+ import { Select } from '@cdc/core/components/EditorPanel/Inputs'
8
+ import { useGlobalContext } from '@cdc/core/components/GlobalContext'
9
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
10
+ import Icon from '@cdc/core/components/ui/Icon'
11
+ import { createCoveId } from '@cdc/core/helpers/createCoveId'
12
+ import { useContext, useEffect, useMemo, useState } from 'react'
13
+ import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
14
+ import { hasConditionalWidgets, normalizeConditionalColumn } from '../helpers/dashboardColumnWidgets'
15
+ import { getDashboardConditionIds } from '../helpers/dashboardConditions'
16
+ import { DASHBOARD_CONDITION_TYPE_OPTIONS, DashboardConditionTypeOption } from '../helpers/dashboardConditionUi'
17
+ import { dashboardConditionsSupportedForRow } from '../helpers/dashboardFilterTargets'
18
+ import { DashboardCondition } from '../types/ConfigRow'
19
+
20
+ import './dashboard-condition-modal.css'
21
+
22
+ type DashboardConditionModalProps = {
23
+ rowIndex: number
24
+ columnIndex?: number
25
+ entryIndex?: number
26
+ }
27
+
28
+ type DashboardConditionFormState = {
29
+ datasetKey: string
30
+ operator: DashboardConditionTypeOption
31
+ columnName: string
32
+ values: string[]
33
+ }
34
+
35
+ const getDashboardConditionFormState = (dashboardCondition?: DashboardCondition): DashboardConditionFormState => ({
36
+ datasetKey: dashboardCondition?.datasetKey || '',
37
+ operator: dashboardCondition?.operator || '',
38
+ columnName: dashboardCondition?.columnName || '',
39
+ values: dashboardCondition?.values || []
40
+ })
41
+
42
+ const tooltipIcon = (label: string) => (
43
+ <Tooltip style={{ textTransform: 'none' }}>
44
+ <Tooltip.Target>
45
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} alt={label} />
46
+ </Tooltip.Target>
47
+ <Tooltip.Content>
48
+ <p className='dashboard-condition-modal__tooltip-text'>{label}</p>
49
+ </Tooltip.Content>
50
+ </Tooltip>
51
+ )
52
+
53
+ export const DashboardConditionModal: React.FC<DashboardConditionModalProps> = ({
54
+ rowIndex,
55
+ columnIndex,
56
+ entryIndex
57
+ }) => {
58
+ const { config } = useContext(DashboardContext)
59
+ const dispatch = useContext(DashboardDispatchContext)
60
+ const { overlay } = useGlobalContext()
61
+ const transform = new DataTransform()
62
+
63
+ const row = config.rows[rowIndex]
64
+ const column = columnIndex === undefined ? undefined : row.columns[columnIndex]
65
+ const isConditionalEntryEditor =
66
+ columnIndex !== undefined && entryIndex !== undefined && hasConditionalWidgets(column)
67
+ const existingDashboardCondition =
68
+ columnIndex === undefined
69
+ ? row.dashboardCondition
70
+ : isConditionalEntryEditor
71
+ ? column?.conditionalWidgets?.[entryIndex]?.dashboardCondition
72
+ : undefined
73
+ const [formState, setFormState] = useState<DashboardConditionFormState>(
74
+ getDashboardConditionFormState(existingDashboardCondition)
75
+ )
76
+ const [datasetRows, setDatasetRows] = useState<Record<string, any>[]>([])
77
+ const [columns, setColumns] = useState<string[]>([])
78
+ const [loadingColumns, setLoadingColumns] = useState(false)
79
+ const [errorMessage, setErrorMessage] = useState('')
80
+ const supportsDashboardConditions = dashboardConditionsSupportedForRow(row)
81
+
82
+ const title =
83
+ columnIndex === undefined
84
+ ? `Row ${rowIndex + 1} Dashboard Condition`
85
+ : isConditionalEntryEditor
86
+ ? `Row ${rowIndex + 1} Column ${columnIndex + 1} Component ${entryIndex + 1} Dashboard Condition`
87
+ : `Row ${rowIndex + 1} Column ${columnIndex + 1} Dashboard Condition`
88
+ const targetLabel = columnIndex === undefined ? 'row' : 'component'
89
+
90
+ const availableDatasets = Object.keys(config.datasets || {})
91
+ const needsValueMatch = formState.operator === 'columnHasAnyValue'
92
+ const usesDashboardFilterState = formState.operator === 'filtersIncomplete'
93
+ const hasCondition = !!formState.operator
94
+ const shouldShowDatasetSelect = hasCondition && !usesDashboardFilterState
95
+ const shouldShowColumnSelect = shouldShowDatasetSelect && needsValueMatch && !!formState.datasetKey
96
+ const shouldShowValueSelect = shouldShowColumnSelect && !!formState.columnName
97
+
98
+ const canSave = useMemo(() => {
99
+ if (!hasCondition) return true
100
+ if (usesDashboardFilterState) return true
101
+ if (!formState.datasetKey || !formState.operator) return false
102
+ if (!needsValueMatch) return true
103
+
104
+ return !!formState.columnName && formState.values.length > 0
105
+ }, [formState, hasCondition, needsValueMatch, usesDashboardFilterState])
106
+
107
+ const updateDashboardCondition = (dashboardCondition?: DashboardCondition) => {
108
+ if (columnIndex === undefined) {
109
+ dispatch({ type: 'UPDATE_ROW', payload: { rowIndex, rowData: { dashboardCondition } } })
110
+ return
111
+ }
112
+
113
+ const columns = row.columns.map((currentColumn, currentColumnIndex) => {
114
+ if (currentColumnIndex !== columnIndex) return currentColumn
115
+
116
+ if (isConditionalEntryEditor) {
117
+ const conditionalWidgets = [...(currentColumn.conditionalWidgets || [])]
118
+ conditionalWidgets[entryIndex] = {
119
+ ...conditionalWidgets[entryIndex],
120
+ dashboardCondition
121
+ }
122
+
123
+ return normalizeConditionalColumn({
124
+ ...currentColumn,
125
+ conditionalWidgets
126
+ })
127
+ }
128
+
129
+ if (!dashboardCondition) {
130
+ return currentColumn
131
+ }
132
+
133
+ return normalizeConditionalColumn({
134
+ ...currentColumn,
135
+ widget: undefined,
136
+ conditionalWidgets: [
137
+ {
138
+ widget: currentColumn.widget,
139
+ dashboardCondition
140
+ }
141
+ ]
142
+ })
143
+ })
144
+ dispatch({ type: 'UPDATE_ROW', payload: { rowIndex, rowData: { columns } } })
145
+ }
146
+
147
+ const closeModal = () => {
148
+ overlay?.actions.toggleOverlay(false)
149
+ }
150
+
151
+ const loadColumns = async (datasetKey: string) => {
152
+ if (!datasetKey) {
153
+ setDatasetRows([])
154
+ setColumns([])
155
+ return
156
+ }
157
+
158
+ const dataset = config.datasets[datasetKey]
159
+ if (!dataset) {
160
+ setDatasetRows([])
161
+ setColumns([])
162
+ return
163
+ }
164
+
165
+ setLoadingColumns(true)
166
+ setErrorMessage('')
167
+
168
+ try {
169
+ let nextData = dataset.data
170
+ if (!nextData && dataset.dataUrl) {
171
+ const response = await fetchRemoteData(dataset.dataUrl)
172
+ nextData = transform.autoStandardize(response.data)
173
+ if (dataset.dataDescription) {
174
+ nextData = transform.developerStandardize(nextData, dataset.dataDescription)
175
+ }
176
+ }
177
+
178
+ setDatasetRows(nextData || [])
179
+ setColumns(Object.keys(nextData?.[0] || {}))
180
+ } catch (_error) {
181
+ setDatasetRows([])
182
+ setColumns([])
183
+ setErrorMessage('There was an issue loading the condition dataset. Please try again.')
184
+ } finally {
185
+ setLoadingColumns(false)
186
+ }
187
+ }
188
+
189
+ useEffect(() => {
190
+ loadColumns(formState.datasetKey)
191
+ }, [formState.datasetKey])
192
+
193
+ const selectedColumnValues = useMemo(() => {
194
+ if (!needsValueMatch || !formState.columnName) return []
195
+
196
+ const distinctValues = datasetRows.reduce((acc, row) => {
197
+ const value = row?.[formState.columnName]
198
+ if (value === undefined || value === null) return acc
199
+
200
+ const normalizedValue = String(value)
201
+ if (!acc.includes(normalizedValue)) {
202
+ acc.push(normalizedValue)
203
+ }
204
+ return acc
205
+ }, [] as string[])
206
+
207
+ return distinctValues
208
+ }, [datasetRows, formState.columnName, needsValueMatch])
209
+
210
+ if (!supportsDashboardConditions) {
211
+ return (
212
+ <Modal>
213
+ <Modal.Content>
214
+ <p>{title} editing is not available for toggle or multi-visualization rows in v1.</p>
215
+ <Button variant='primary' onClick={closeModal}>
216
+ Close
217
+ </Button>
218
+ </Modal.Content>
219
+ </Modal>
220
+ )
221
+ }
222
+
223
+ return (
224
+ <Modal>
225
+ <Modal.Content>
226
+ {loadingColumns && <Loader fullScreen />}
227
+ <div className='dashboard-condition-modal'>
228
+ <h3>{title}</h3>
229
+
230
+ <div className='dashboard-condition-modal__fields'>
231
+ <Select
232
+ className='dashboard-condition-modal__select py-2 ps-2 w-100 d-block'
233
+ fieldName='operator'
234
+ label='Condition Type'
235
+ options={DASHBOARD_CONDITION_TYPE_OPTIONS}
236
+ tooltip={tooltipIcon(
237
+ `Choose whether this ${targetLabel} should appear when the filtered condition dataset has data, has no data, contains one of the selected column values, or when targeted filters are incomplete. Use "Show when filters are incomplete" for static helper content, such as a markup include message explaining that filters must be selected to proceed.`
238
+ )}
239
+ value={formState.operator}
240
+ onChange={event => {
241
+ const operator = event.target.value as DashboardConditionTypeOption
242
+ setFormState(currentState => ({
243
+ ...currentState,
244
+ operator,
245
+ datasetKey: operator && operator !== 'filtersIncomplete' ? currentState.datasetKey : '',
246
+ columnName: operator === 'columnHasAnyValue' ? currentState.columnName : '',
247
+ values: operator === 'columnHasAnyValue' ? currentState.values : []
248
+ }))
249
+ }}
250
+ />
251
+
252
+ {shouldShowDatasetSelect && (
253
+ <>
254
+ <Select
255
+ className='dashboard-condition-modal__select py-2 ps-2 w-100 d-block'
256
+ fieldName='datasetKey'
257
+ label='Condition Dataset'
258
+ options={[
259
+ { value: '', label: '- Select Option -' },
260
+ ...availableDatasets.map(key => ({ value: key, label: key }))
261
+ ]}
262
+ value={formState.datasetKey}
263
+ onChange={event => {
264
+ const datasetKey = event.target.value
265
+ setFormState(currentState => ({
266
+ ...currentState,
267
+ datasetKey,
268
+ columnName: datasetKey === currentState.datasetKey ? currentState.columnName : '',
269
+ values: datasetKey === currentState.datasetKey ? currentState.values : []
270
+ }))
271
+ }}
272
+ />
273
+
274
+ {shouldShowColumnSelect && (
275
+ <>
276
+ <Select
277
+ className='dashboard-condition-modal__select py-2 ps-2 w-100 d-block'
278
+ fieldName='columnName'
279
+ label='Column'
280
+ options={[
281
+ { value: '', label: '- Select Option -' },
282
+ ...columns.map(columnName => ({ value: columnName, label: columnName }))
283
+ ]}
284
+ tooltip={tooltipIcon('Select the dataset column to inspect for this condition.')}
285
+ value={formState.columnName}
286
+ onChange={event => {
287
+ const columnName = event.target.value
288
+ setFormState(currentState => ({
289
+ ...currentState,
290
+ columnName,
291
+ values: columnName === currentState.columnName ? currentState.values : []
292
+ }))
293
+ }}
294
+ />
295
+
296
+ {shouldShowValueSelect && (
297
+ <div className='dashboard-condition-modal__multiselect-field'>
298
+ <span className='edit-label column-heading'>
299
+ Column Values
300
+ {tooltipIcon(
301
+ 'Choose one or more matching values from the selected column. This condition passes when the filtered dataset contains at least one row with one of these values.'
302
+ )}
303
+ </span>
304
+ <MultiSelect
305
+ fieldName='values'
306
+ options={selectedColumnValues.map(value => ({ value, label: value }))}
307
+ selected={formState.values}
308
+ updateField={(_section, _subSection, _fieldName, values) => {
309
+ setFormState(currentState => ({
310
+ ...currentState,
311
+ values
312
+ }))
313
+ }}
314
+ />
315
+ </div>
316
+ )}
317
+ </>
318
+ )}
319
+ </>
320
+ )}
321
+ </div>
322
+
323
+ {errorMessage && <p className='text-danger'>{errorMessage}</p>}
324
+
325
+ <div className='d-flex gap-2 mt-3'>
326
+ <Button
327
+ disabled={!canSave}
328
+ onClick={() => {
329
+ if (!hasCondition || !formState.operator) {
330
+ updateDashboardCondition(undefined)
331
+ closeModal()
332
+ return
333
+ }
334
+
335
+ const nextCondition: DashboardCondition = {
336
+ id:
337
+ existingDashboardCondition?.id ||
338
+ createCoveId('condition', { existingIds: getDashboardConditionIds(config.rows) }),
339
+ operator: formState.operator
340
+ }
341
+
342
+ if (!usesDashboardFilterState) {
343
+ nextCondition.datasetKey = formState.datasetKey
344
+ }
345
+
346
+ if (needsValueMatch) {
347
+ nextCondition.columnName = formState.columnName
348
+ nextCondition.values = formState.values
349
+ }
350
+
351
+ updateDashboardCondition(nextCondition)
352
+ closeModal()
353
+ }}
354
+ variant='primary'
355
+ >
356
+ Save
357
+ </Button>
358
+
359
+ <Button onClick={closeModal} variant='secondary'>
360
+ Cancel
361
+ </Button>
362
+ </div>
363
+ </div>
364
+ </Modal.Content>
365
+ </Modal>
366
+ )
367
+ }
@@ -0,0 +1,59 @@
1
+ import React from 'react'
2
+ import Button from '@cdc/core/components/elements/Button'
3
+ import { useGlobalContext } from '@cdc/core/components/GlobalContext'
4
+ import { iconHash } from '../helpers/iconHash'
5
+ import { getColumnHasAnyValueSummaryParts, getDashboardConditionSummary } from '../helpers/dashboardConditionUi'
6
+ import { DashboardCondition } from '../types/ConfigRow'
7
+ import { DashboardConditionModal } from './DashboardConditionModal'
8
+
9
+ import './dashboard-condition-summary.css'
10
+
11
+ type DashboardConditionSummaryProps = {
12
+ dashboardCondition: DashboardCondition
13
+ rowIndex: number
14
+ columnIndex?: number
15
+ entryIndex?: number
16
+ className?: string
17
+ }
18
+
19
+ export const DashboardConditionSummary: React.FC<DashboardConditionSummaryProps> = ({
20
+ dashboardCondition,
21
+ rowIndex,
22
+ columnIndex,
23
+ entryIndex,
24
+ className = ''
25
+ }) => {
26
+ const { overlay } = useGlobalContext()
27
+ const summary = getDashboardConditionSummary(dashboardCondition)
28
+ const columnHasAnyValueSummaryParts =
29
+ dashboardCondition.operator === 'columnHasAnyValue'
30
+ ? getColumnHasAnyValueSummaryParts(dashboardCondition.columnName)
31
+ : undefined
32
+ const summaryContent = columnHasAnyValueSummaryParts ? (
33
+ <>
34
+ {columnHasAnyValueSummaryParts.prefix}
35
+ <strong>{columnHasAnyValueSummaryParts.columnName}</strong>
36
+ {columnHasAnyValueSummaryParts.suffix}
37
+ </>
38
+ ) : (
39
+ summary
40
+ )
41
+
42
+ return (
43
+ <Button
44
+ aria-label={`Configure Dashboard Condition: ${summary}`}
45
+ className={['dashboard-condition-summary', className].filter(Boolean).join(' ')}
46
+ title={summary}
47
+ onClick={() => {
48
+ overlay?.actions.openOverlay(
49
+ <DashboardConditionModal rowIndex={rowIndex} columnIndex={columnIndex} entryIndex={entryIndex} />
50
+ )
51
+ }}
52
+ >
53
+ <span className='dashboard-condition-summary__icon' aria-hidden='true'>
54
+ {iconHash['condition']}
55
+ </span>
56
+ <span className='dashboard-condition-summary__text'>{summaryContent}</span>
57
+ </Button>
58
+ )
59
+ }
@@ -17,6 +17,8 @@ type DashboardEditorProps = {
17
17
  _updateConfig: (config: any) => void
18
18
  isDebug?: boolean
19
19
  setSharedFilter?: Function
20
+ clearSharedFilter?: (key: string) => void
21
+ hasActiveSharedFilter?: (key: string) => boolean
20
22
  apiFilterDropdowns?: APIFilterDropdowns
21
23
  state: DashboardState
22
24
  interactionLabel: string
@@ -28,6 +30,8 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
28
30
  _updateConfig,
29
31
  isDebug,
30
32
  setSharedFilter,
33
+ clearSharedFilter,
34
+ hasActiveSharedFilter,
31
35
  apiFilterDropdowns,
32
36
  state,
33
37
  interactionLabel = ''
@@ -63,6 +67,10 @@ const DashboardEditors: React.FC<DashboardEditorProps> = ({
63
67
  isDebug={isDebug}
64
68
  setConfig={_updateConfig}
65
69
  setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
70
+ clearSharedFilter={setsSharedFilter ? clearSharedFilter : undefined}
71
+ hasActiveSharedFilter={
72
+ setsSharedFilter && hasActiveSharedFilter ? hasActiveSharedFilter(visualizationKey) : false
73
+ }
66
74
  setSharedFilterValue={setSharedFilterValue}
67
75
  isDashboard={true}
68
76
  showLoader={false}
@@ -1,4 +1,4 @@
1
- import { fireEvent, render } from '@testing-library/react'
1
+ import { fireEvent, render, screen } from '@testing-library/react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
  import DashboardFilters from './DashboardFilters'
4
4
 
@@ -127,3 +127,141 @@ describe('DashboardFilters nested dropdown display', () => {
127
127
  expect(handleOnChange).toHaveBeenCalledWith(0, ['2024', 'Q3'])
128
128
  })
129
129
  })
130
+
131
+ describe('DashboardFilters layout', () => {
132
+ const createDropdownFilter = () =>
133
+ ({
134
+ key: 'State',
135
+ type: 'datafilter',
136
+ filterStyle: 'dropdown',
137
+ showDropdown: true,
138
+ values: ['Alabama', 'Alaska'],
139
+ columnName: 'state',
140
+ id: 0,
141
+ parents: [],
142
+ order: 'asc',
143
+ active: 'Alabama'
144
+ } as any)
145
+
146
+ it('keeps intro text outside the gapped controls form', () => {
147
+ const { container } = render(
148
+ <DashboardFilters
149
+ applyFilters={vi.fn()}
150
+ apiFilterDropdowns={{}}
151
+ filterIntro='Choose a <strong>state</strong>.'
152
+ filters={[createDropdownFilter()]}
153
+ handleOnChange={vi.fn()}
154
+ show={[0]}
155
+ showSubmit={true}
156
+ />
157
+ )
158
+
159
+ const intro = container.querySelector('.filters-section__intro-text')
160
+ const form = container.querySelector('.dashboard-filters__form')
161
+
162
+ expect(intro).toBeInTheDocument()
163
+ expect(intro?.querySelector('strong')).toHaveTextContent('state')
164
+ expect(form).toBeInTheDocument()
165
+ expect(form).not.toContainElement(intro as Element)
166
+ expect(form?.querySelector('.dashboard-filters__field')).toBeInTheDocument()
167
+ expect(form?.querySelector('.dashboard-filters__actions')).toBeInTheDocument()
168
+ expect(intro?.compareDocumentPosition(form as Element) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
169
+ })
170
+ })
171
+
172
+ describe('DashboardFilters filter notes', () => {
173
+ const createDropdownFilter = (note?: string) =>
174
+ ({
175
+ key: 'State',
176
+ type: 'datafilter',
177
+ filterStyle: 'dropdown',
178
+ showDropdown: true,
179
+ values: ['Alabama', 'Alaska'],
180
+ columnName: 'state',
181
+ id: 0,
182
+ parents: [],
183
+ order: 'asc',
184
+ active: 'Alabama',
185
+ note
186
+ } as any)
187
+
188
+ const renderDashboardFilterList = (filters, show = filters.map((_filter, index) => index)) =>
189
+ render(
190
+ <DashboardFilters
191
+ applyFilters={vi.fn()}
192
+ apiFilterDropdowns={{}}
193
+ filters={filters}
194
+ handleOnChange={vi.fn()}
195
+ show={show}
196
+ showSubmit={false}
197
+ />
198
+ )
199
+ const renderDashboardFilters = filter => renderDashboardFilterList([filter])
200
+
201
+ it('renders parsed HTML notes under the label and before dropdown controls', () => {
202
+ const { container } = renderDashboardFilters(createDropdownFilter('Choose a <strong>state</strong>.'))
203
+
204
+ const label = screen.getByText('State')
205
+ const note = container.querySelector('.filters-section__note-text')
206
+ const select = screen.getByLabelText('State')
207
+
208
+ expect(note).toBeInTheDocument()
209
+ expect(note).toHaveTextContent('Choose a state.')
210
+ expect(note?.querySelector('strong')).toHaveTextContent('state')
211
+ expect(label.compareDocumentPosition(note as Element) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
212
+ expect(note?.compareDocumentPosition(select) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
213
+ expect(select).toHaveClass('filters-section__select--fit-content')
214
+ expect(select).not.toHaveClass('w-100')
215
+ })
216
+
217
+ it('renders notes for tab-simple filters above the tab control', () => {
218
+ const { container } = renderDashboardFilters({
219
+ ...createDropdownFilter('Pick <em>status</em>.'),
220
+ key: 'Status',
221
+ filterStyle: 'tab-simple',
222
+ values: ['Current', 'Prior'],
223
+ active: 'Current'
224
+ })
225
+
226
+ const note = container.querySelector('.filters-section__note-text')
227
+ const tab = screen.getByRole('button', { name: 'Current' })
228
+
229
+ expect(note).toHaveTextContent('Pick status.')
230
+ expect(note?.querySelector('em')).toHaveTextContent('status')
231
+ expect(note?.compareDocumentPosition(tab) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
232
+ })
233
+
234
+ it('does not render note markup for empty notes', () => {
235
+ const { container } = renderDashboardFilters(createDropdownFilter(' '))
236
+ const select = screen.getByLabelText('State')
237
+
238
+ expect(container.querySelector('.filters-section__note-text')).not.toBeInTheDocument()
239
+ expect(select).toHaveClass('w-100')
240
+ expect(select).not.toHaveClass('filters-section__select--fit-content')
241
+ })
242
+
243
+ it('marks the form as single-filter layout when only one dashboard filter is visible', () => {
244
+ const hiddenFilter = { ...createDropdownFilter(), key: 'Hidden State', showDropdown: false }
245
+ const { container } = renderDashboardFilterList([createDropdownFilter('Choose a state.'), hiddenFilter])
246
+
247
+ const form = container.querySelector('.dashboard-filters__form')
248
+
249
+ expect(form).toHaveClass('filters-section__wrapper--single')
250
+ expect(form).not.toHaveClass('filters-section__wrapper--multiple')
251
+ })
252
+
253
+ it('marks the form as multiple-filter layout when more than one dashboard filter is visible', () => {
254
+ const statusFilter = {
255
+ ...createDropdownFilter('Choose a status.'),
256
+ key: 'Status',
257
+ columnName: 'status',
258
+ active: 'Current'
259
+ }
260
+ const { container } = renderDashboardFilterList([createDropdownFilter('Choose a state.'), statusFilter])
261
+
262
+ const form = container.querySelector('.dashboard-filters__form')
263
+
264
+ expect(form).toHaveClass('filters-section__wrapper--multiple')
265
+ expect(form).not.toHaveClass('filters-section__wrapper--single')
266
+ })
267
+ })