@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
@@ -13,6 +13,8 @@ type HeaderProps = {
13
13
  visualizationKey?: string
14
14
  }
15
15
 
16
+ type DownloadImageMode = 'off' | 'button' | 'link'
17
+
16
18
  const Header = (props: HeaderProps) => {
17
19
  const tabs: Tab[] = ['Dashboard Description', 'Data Table Settings', 'Dashboard Preview']
18
20
  const { visualizationKey, subEditor } = props
@@ -51,7 +53,7 @@ const Header = (props: HeaderProps) => {
51
53
  return acc
52
54
  }, {})
53
55
 
54
- dispatch({ type: 'SET_DATA', payload: sampleDataRemoved })
56
+ dispatch({ type: 'SET_DATA', payload: { data: sampleDataRemoved } })
55
57
  }
56
58
  }
57
59
 
@@ -62,6 +64,18 @@ const Header = (props: HeaderProps) => {
62
64
  dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
63
65
  }
64
66
 
67
+ const getDownloadImageMode = (): DownloadImageMode => {
68
+ if (!config.table?.downloadImageButton) return 'off'
69
+ return config.table.downloadImageButtonStyle === 'link' ? 'link' : 'button'
70
+ }
71
+
72
+ const changeDownloadImageMode = (mode: DownloadImageMode) => {
73
+ const newConfig = { ...config, table: { ...(config.table || {}) } }
74
+ newConfig.table.downloadImageButton = mode !== 'off'
75
+ if (mode !== 'off') newConfig.table.downloadImageButtonStyle = mode
76
+ dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })
77
+ }
78
+
65
79
  const convertStateToConfig = () => {
66
80
  const strippedState = cloneConfig(config)
67
81
  delete strippedState.newViz
@@ -231,22 +245,27 @@ const Header = (props: HeaderProps) => {
231
245
  />
232
246
  Show URL to Automatically Updated Data
233
247
  </label>
234
- <label>
235
- <input
236
- type='checkbox'
237
- defaultChecked={config.table.downloadImageButton}
238
- onChange={e => changeConfigValue('table', 'downloadImageButton', e.target.checked)}
239
- />
240
- Show Download Image Button
241
- </label>
242
- {config.table.downloadImageButton && (
243
- <input
244
- type='text'
245
- placeholder='Customize label'
246
- defaultValue={config.table.downloadImageLabel}
247
- onChange={e => changeConfigValue('table', 'downloadImageLabel', e.target.value)}
248
- />
249
- )}
248
+ <div className='download-image-controls'>
249
+ <select
250
+ aria-label='Download image display'
251
+ className='download-image-mode-select'
252
+ value={getDownloadImageMode()}
253
+ onChange={e => changeDownloadImageMode(e.target.value as DownloadImageMode)}
254
+ >
255
+ <option value='off'>Download Image Off</option>
256
+ <option value='button'>Download Image Button</option>
257
+ <option value='link'>Download Image Link</option>
258
+ </select>
259
+ {getDownloadImageMode() !== 'off' && (
260
+ <input
261
+ className='download-image-label-input'
262
+ type='text'
263
+ placeholder='Customize label'
264
+ defaultValue={config.table.downloadImageLabel}
265
+ onChange={e => changeConfigValue('table', 'downloadImageLabel', e.target.value)}
266
+ />
267
+ )}
268
+ </div>
250
269
  </div>
251
270
  </>
252
271
  )}
@@ -0,0 +1,228 @@
1
+ import React from 'react'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import { DashboardContext, DashboardDispatchContext, initialState } from '../DashboardContext'
5
+ import { GlobalContext } from '@cdc/core/components/GlobalContext'
6
+ import Row from './Row'
7
+
8
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
9
+ default: props => <span data-testid='mock-icon' {...props} />
10
+ }))
11
+
12
+ vi.mock('../images/icon-col-12.svg', () => ({ default: () => <svg data-testid='icon-col-12' /> }))
13
+ vi.mock('../images/icon-col-6.svg', () => ({ default: () => <svg data-testid='icon-col-6' /> }))
14
+ vi.mock('../images/icon-col-4.svg', () => ({ default: () => <svg data-testid='icon-col-4' /> }))
15
+ vi.mock('../images/icon-col-4-8.svg', () => ({ default: () => <svg data-testid='icon-col-4-8' /> }))
16
+ vi.mock('../images/icon-col-8-4.svg', () => ({ default: () => <svg data-testid='icon-col-8-4' /> }))
17
+ vi.mock('../images/icon-toggle.svg', () => ({ default: () => <svg data-testid='icon-toggle' /> }))
18
+
19
+ const renderRow = (dashboardCondition = undefined) => {
20
+ const openOverlay = vi.fn()
21
+
22
+ render(
23
+ <GlobalContext.Provider
24
+ value={{
25
+ overlay: {
26
+ object: null,
27
+ show: false,
28
+ disableBgClose: false,
29
+ actions: {
30
+ openOverlay,
31
+ toggleOverlay: vi.fn()
32
+ }
33
+ }
34
+ }}
35
+ >
36
+ <DashboardContext.Provider
37
+ value={{
38
+ ...initialState,
39
+ config: {
40
+ type: 'dashboard',
41
+ dashboard: { sharedFilters: [] },
42
+ datasets: {},
43
+ rows: [
44
+ {
45
+ columns: [],
46
+ dashboardCondition,
47
+ expandCollapseAllButtons: false
48
+ }
49
+ ],
50
+ visualizations: {}
51
+ } as any,
52
+ outerContainerRef: vi.fn(),
53
+ setParentConfig: vi.fn(),
54
+ isDebug: false,
55
+ isEditor: true,
56
+ reloadURLData: vi.fn(),
57
+ loadAPIFilters: vi.fn(),
58
+ setAPIFilterDropdowns: vi.fn(),
59
+ setAPILoading: vi.fn()
60
+ }}
61
+ >
62
+ <DashboardDispatchContext.Provider value={vi.fn()}>
63
+ <Row row={{ columns: [], dashboardCondition, expandCollapseAllButtons: false } as any} idx={0} uuid='row-1' />
64
+ </DashboardDispatchContext.Provider>
65
+ </DashboardContext.Provider>
66
+ </GlobalContext.Provider>
67
+ )
68
+
69
+ return { openOverlay }
70
+ }
71
+
72
+ const renderRowWithConfig = config => {
73
+ const dispatch = vi.fn()
74
+
75
+ render(
76
+ <GlobalContext.Provider
77
+ value={{
78
+ overlay: {
79
+ object: null,
80
+ show: false,
81
+ disableBgClose: false,
82
+ actions: {
83
+ openOverlay: vi.fn(),
84
+ toggleOverlay: vi.fn()
85
+ }
86
+ }
87
+ }}
88
+ >
89
+ <DashboardContext.Provider
90
+ value={{
91
+ ...initialState,
92
+ config,
93
+ outerContainerRef: vi.fn(),
94
+ setParentConfig: vi.fn(),
95
+ isDebug: false,
96
+ isEditor: true,
97
+ reloadURLData: vi.fn(),
98
+ loadAPIFilters: vi.fn(),
99
+ setAPIFilterDropdowns: vi.fn(),
100
+ setAPILoading: vi.fn()
101
+ }}
102
+ >
103
+ <DashboardDispatchContext.Provider value={dispatch}>
104
+ <Row row={config.rows[0]} idx={0} uuid='row-1' />
105
+ </DashboardDispatchContext.Provider>
106
+ </DashboardContext.Provider>
107
+ </GlobalContext.Provider>
108
+ )
109
+
110
+ return { dispatch }
111
+ }
112
+
113
+ describe('Row', () => {
114
+ it('renders the row label without a separator', () => {
115
+ renderRow()
116
+
117
+ expect(screen.getByText('Row 1')).toBeInTheDocument()
118
+ expect(screen.queryByText('Row - 1')).not.toBeInTheDocument()
119
+ })
120
+
121
+ it('renders separate row toolbar buttons for data and dashboard conditions', () => {
122
+ const { openOverlay } = renderRow()
123
+
124
+ fireEvent.click(screen.getByTitle('Configure Data'))
125
+ fireEvent.click(screen.getByTitle('Configure Dashboard Condition'))
126
+
127
+ expect(openOverlay).toHaveBeenCalledTimes(2)
128
+ })
129
+
130
+ it('does not render a row condition summary when no condition exists', () => {
131
+ renderRow()
132
+
133
+ expect(screen.getByTitle('Configure Dashboard Condition')).toBeInTheDocument()
134
+ expect(screen.queryByRole('button', { name: /Configure Dashboard Condition:/ })).not.toBeInTheDocument()
135
+ })
136
+
137
+ it('shows the active row condition button and summary strip when a condition exists', () => {
138
+ renderRow({ id: 'row-condition-1', datasetKey: 'condition-data', operator: 'hasData' })
139
+
140
+ expect(screen.getByTitle('Configure Dashboard Condition')).toHaveClass('is-active')
141
+ expect(screen.getByRole('button', { name: "Configure Dashboard Condition: Show when there's data" })).toHaveClass(
142
+ 'dashboard-condition-summary'
143
+ )
144
+ })
145
+
146
+ it('opens the condition modal from the row condition button or summary strip', () => {
147
+ const { openOverlay } = renderRow({ id: 'row-condition-1', datasetKey: 'condition-data', operator: 'hasData' })
148
+
149
+ fireEvent.click(screen.getByTitle('Configure Dashboard Condition'))
150
+ fireEvent.click(screen.getByRole('button', { name: "Configure Dashboard Condition: Show when there's data" }))
151
+
152
+ expect(openOverlay).toHaveBeenCalledTimes(2)
153
+ })
154
+
155
+ it('remaps row targets and preserves unknown targets when deleting a row', () => {
156
+ const { dispatch } = renderRowWithConfig({
157
+ type: 'dashboard',
158
+ dashboard: {
159
+ sharedFilters: [
160
+ {
161
+ key: 'County',
162
+ type: 'datafilter',
163
+ columnName: 'county',
164
+ usedBy: ['legacy-footnote-target', 'viz-1', 0, 1]
165
+ }
166
+ ]
167
+ },
168
+ datasets: {},
169
+ rows: [
170
+ {
171
+ columns: [],
172
+ dashboardCondition: { id: 'row-condition-1', datasetKey: 'condition-data', operator: 'hasData' },
173
+ expandCollapseAllButtons: false
174
+ },
175
+ {
176
+ columns: [{ width: 12, widget: 'dashboard-filters-1' }],
177
+ dashboardCondition: { id: 'condition-1', datasetKey: 'condition-data', operator: 'hasData' },
178
+ expandCollapseAllButtons: false
179
+ }
180
+ ],
181
+ visualizations: {
182
+ 'dashboard-filters-1': {
183
+ uid: 'dashboard-filters-1',
184
+ type: 'dashboardFilters',
185
+ sharedFilterIndexes: [0]
186
+ }
187
+ }
188
+ } as any)
189
+
190
+ fireEvent.click(screen.getByTitle('Delete Row'))
191
+
192
+ const nextConfig = dispatch.mock.calls[0][0].payload[0]
193
+ expect(nextConfig.dashboard.sharedFilters[0].usedBy).toEqual(['legacy-footnote-target', 'viz-1', 0])
194
+ })
195
+
196
+ it('assigns distinct row uuids when moving a row even if Date.now matches', () => {
197
+ const mathRandomSpy = vi.spyOn(Math, 'random')
198
+ mathRandomSpy.mockReturnValueOnce(0.123456789).mockReturnValueOnce(0.23456789)
199
+
200
+ const { dispatch } = renderRowWithConfig({
201
+ type: 'dashboard',
202
+ dashboard: { sharedFilters: [] },
203
+ datasets: {},
204
+ rows: [
205
+ {
206
+ uuid: 'row-a',
207
+ columns: [],
208
+ expandCollapseAllButtons: false
209
+ },
210
+ {
211
+ uuid: 'row-b',
212
+ columns: [],
213
+ expandCollapseAllButtons: false
214
+ }
215
+ ],
216
+ visualizations: {}
217
+ } as any)
218
+
219
+ fireEvent.click(screen.getByTitle('Move Row Down'))
220
+
221
+ const nextConfig = dispatch.mock.calls[0][0].payload[0]
222
+ expect(nextConfig.rows[0].uuid).not.toEqual(nextConfig.rows[1].uuid)
223
+ expect(nextConfig.rows[0].uuid).toMatch(/^row-[a-z0-9]{8}$/)
224
+ expect(nextConfig.rows[1].uuid).toMatch(/^row-[a-z0-9]{8}$/)
225
+
226
+ mathRandomSpy.mockRestore()
227
+ })
228
+ })
@@ -14,12 +14,18 @@ import EightFourColIcon from '../images/icon-col-8-4.svg'
14
14
  import ToggleIcon from '../images/icon-toggle.svg'
15
15
  import { ConfigRow } from '../types/ConfigRow'
16
16
  import { DataDesignerModal } from './DataDesignerModal'
17
+ import { DashboardConditionModal } from './DashboardConditionModal'
18
+ import { DashboardConditionSummary } from './DashboardConditionSummary'
17
19
  import { useGlobalContext } from '@cdc/core/components/GlobalContext'
18
20
  import Button from '@cdc/core/components/elements/Button'
19
21
  import { iconHash } from '../helpers/iconHash'
20
22
  import _ from 'lodash'
21
23
  import { Visualization } from '@cdc/core/types/Visualization'
22
24
  import { labelHash } from '@cdc/core/helpers/labelHash'
25
+ import { removeDashboardFilter } from '../helpers/removeDashboardFilter'
26
+ import { dashboardConditionsSupportedForRow, remapRowTargetsInSharedFilters } from '../helpers/dashboardFilterTargets'
27
+ import { getColumnPrimaryWidget, getColumnWidgetKeys } from '../helpers/dashboardColumnWidgets'
28
+ import { createCoveId } from '@cdc/core/helpers/createCoveId'
23
29
 
24
30
  type RowMenuProps = {
25
31
  rowIdx: number
@@ -46,7 +52,7 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
46
52
  const newRows = _.cloneDeep(rows)
47
53
  newRows[rowIdx].toggle = toggle
48
54
  const rowColumns = newRows[rowIdx].columns
49
- const columnsWithWidgets = rowColumns.filter(c => c.widget)
55
+ const columnsWithWidgets = rowColumns.filter(c => getColumnWidgetKeys(c).length > 0)
50
56
 
51
57
  const totalWidgets = columnsWithWidgets.length
52
58
  if (totalWidgets > layout.length) {
@@ -59,7 +65,8 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
59
65
 
60
66
  // Adds placeholder column name that defaults to the visualization type.
61
67
  newRows[rowIdx].columns.forEach((col, idx) => {
62
- col.toggleName = col.toggleName || labelHash[config.visualizations[col.widget]?.type] || undefined
68
+ const primaryWidget = getColumnPrimaryWidget(col)
69
+ col.toggleName = col.toggleName || labelHash[config.visualizations[primaryWidget]?.type] || undefined
63
70
  })
64
71
 
65
72
  newRows[rowIdx].columns = layout.map((width, colIndex) => {
@@ -88,10 +95,20 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
88
95
  rows[newIdx] = row
89
96
  rows[rowIdx] = temp
90
97
 
91
- rows[newIdx].uuid = Date.now()
92
- rows[rowIdx].uuid = Date.now()
98
+ const existingRowUuids = rows.map(row => row.uuid).filter(uuid => uuid !== undefined)
99
+ rows[newIdx].uuid = createCoveId('row', { existingIds: existingRowUuids })
100
+ rows[rowIdx].uuid = createCoveId('row', { existingIds: [...existingRowUuids, rows[newIdx].uuid] })
93
101
 
94
- updateConfig({ ...config, rows })
102
+ const remappedSharedFilters = remapRowTargetsInSharedFilters(
103
+ config.dashboard.sharedFilters || [],
104
+ targetRowIndex => {
105
+ if (targetRowIndex === rowIdx) return newIdx
106
+ if (targetRowIndex === newIdx) return rowIdx
107
+ return targetRowIndex
108
+ }
109
+ )
110
+
111
+ updateConfig({ ...config, rows, dashboard: { ...config.dashboard, sharedFilters: remappedSharedFilters } })
95
112
 
96
113
  // TODO: Migrate this animation to a React animation library once one is selected for COVE. This is pretty minor so can stay for now.
97
114
  let calcRowMove = dir === 'down' ? 202 : -202
@@ -100,6 +117,8 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
100
117
  let rowEle = document.querySelector("[data-row-id='" + rowIdx + "']") as HTMLElement
101
118
  let rowNewEle = document.querySelector("[data-row-id='" + newIdx + "']") as HTMLElement
102
119
 
120
+ if (!rowEle || !rowNewEle) return
121
+
103
122
  rowEle.style.pointerEvents = 'none'
104
123
  rowNewEle.style.pointerEvents = 'none'
105
124
  rowEle.style.top = calcRowMove + 'px'
@@ -120,19 +139,47 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
120
139
 
121
140
  const deleteRow = () => {
122
141
  let newVisualizations = { ...config.visualizations }
142
+ let newSharedFilters = remapRowTargetsInSharedFilters(config.dashboard.sharedFilters || [], targetRowIndex => {
143
+ if (targetRowIndex === rowIdx) return null
144
+ if (targetRowIndex > rowIdx) return targetRowIndex - 1
145
+ return targetRowIndex
146
+ })
123
147
 
124
- //delete the instantiated widgets
125
- if (rows[rowIdx] && rows[rowIdx].columns && rows[rowIdx].columns.length && config.visualizations) {
148
+ if (rows[rowIdx]?.columns?.length && config.visualizations) {
126
149
  rows[rowIdx].columns.forEach(column => {
127
- if (column.widget) {
128
- delete newVisualizations[column.widget]
129
- }
150
+ getColumnWidgetKeys(column).forEach(widgetKey => {
151
+ delete newVisualizations[widgetKey]
152
+ newSharedFilters.forEach(sharedFilter => {
153
+ if (sharedFilter.usedBy) {
154
+ sharedFilter.usedBy = sharedFilter.usedBy.filter(uid => uid !== widgetKey)
155
+ }
156
+ })
157
+ })
130
158
  })
131
159
  }
132
160
 
133
- rows.splice(rowIdx, 1) // delete the row
161
+ rows.splice(rowIdx, 1)
162
+
163
+ // Remove shared filters no longer referenced by any remaining dashboardFilters widget,
164
+ // iterating in reverse so removals don't invalidate earlier indices. removeDashboardFilter
165
+ // shifts sharedFilterIndexes in all remaining vizs so indices stay consistent.
166
+ let currentFilters = newSharedFilters
167
+ let currentVizs = newVisualizations
168
+ for (let i = currentFilters.length - 1; i >= 0; i--) {
169
+ const isReferenced = Object.values(currentVizs).some(
170
+ v => v.type === 'dashboardFilters' && (v as any).sharedFilterIndexes?.includes(i)
171
+ )
172
+ if (!isReferenced) {
173
+ ;[currentFilters, currentVizs] = removeDashboardFilter(i, currentFilters, currentVizs as any)
174
+ }
175
+ }
134
176
 
135
- updateConfig({ ...config, rows, visualizations: newVisualizations })
177
+ updateConfig({
178
+ ...config,
179
+ rows,
180
+ visualizations: currentVizs,
181
+ dashboard: { ...config.dashboard, sharedFilters: currentFilters }
182
+ })
136
183
  }
137
184
 
138
185
  const layoutList = [
@@ -193,7 +240,8 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
193
240
  <ul className='row-menu__flyout'>{layoutList}</ul>
194
241
  {isMultiColumn && (
195
242
  <Button
196
- className={`btn row-menu__btn border-0${row.equalHeight ? ' btn-primary' : ''}`}
243
+ variant={row.equalHeight ? 'primary' : undefined}
244
+ className='row-menu__btn border-0'
197
245
  title={row.equalHeight ? 'Disable Equal Height Rows' : 'Enable Equal Height Rows'}
198
246
  onClick={toggleEqualHeight}
199
247
  >
@@ -206,7 +254,8 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
206
254
  )}
207
255
  <div className='spacer'></div>
208
256
  <Button
209
- className={`btn btn-primary row-menu__btn border-0`}
257
+ variant='primary'
258
+ className='row-menu__btn border-0'
210
259
  title='Move Row Up'
211
260
  onClick={() => moveRow('up')}
212
261
  disabled={rowIdx === 0}
@@ -214,7 +263,8 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
214
263
  <Icon display='caretUp' color='#fff' size={25} />
215
264
  </Button>
216
265
  <Button
217
- className={'btn btn-primary row-menu__btn border-0'}
266
+ variant='primary'
267
+ className='row-menu__btn border-0'
218
268
  title='Move Row Down'
219
269
  onClick={() => moveRow('down')}
220
270
  disabled={rowIdx + 1 === rows.length}
@@ -222,7 +272,8 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
222
272
  <Icon display='caretDown' color='#fff' size={25} />
223
273
  </Button>
224
274
  <Button
225
- className={'btn btn-danger row-menu__btn row-menu__btn--remove border-0'}
275
+ variant='danger'
276
+ className='row-menu__btn row-menu__btn--remove border-0'
226
277
  title='Delete Row'
227
278
  onClick={deleteRow}
228
279
  disabled={rowIdx === 0 && rows.length === 1}
@@ -237,20 +288,44 @@ type RowProps = { row: ConfigRow; idx: number; uuid: number | string }
237
288
 
238
289
  const Row: React.FC<RowProps> = ({ row, idx: rowIdx, uuid }) => {
239
290
  const { overlay } = useGlobalContext()
291
+ const supportsDashboardConditions = dashboardConditionsSupportedForRow(row)
292
+ const hasDashboardCondition = !!row.dashboardCondition
293
+
240
294
  return (
241
295
  <>
242
296
  <div className='builder-row' data-row-id={rowIdx}>
243
297
  <RowMenu rowIdx={rowIdx} />
244
- <span className='ms-2 mt-n3'>Row - {rowIdx + 1}</span>
298
+ <span className='builder-row__label'>Row {rowIdx + 1}</span>
245
299
  <Button
246
300
  title='Configure Data'
247
- className='btn btn-configure-row'
301
+ className='btn-configure-row btn-configure-row--data'
248
302
  onClick={() => {
249
303
  overlay?.actions.openOverlay(<DataDesignerModal rowIndex={rowIdx} />)
250
304
  }}
251
305
  >
252
306
  {iconHash['gearMulti']}
253
307
  </Button>
308
+ <Button
309
+ title={
310
+ supportsDashboardConditions
311
+ ? 'Configure Dashboard Condition'
312
+ : 'Dashboard conditions are not available for toggle or multi-visualization rows'
313
+ }
314
+ className={`btn-configure-row btn-configure-row--condition${hasDashboardCondition ? ' is-active' : ''}`}
315
+ disabled={!supportsDashboardConditions}
316
+ onClick={() => {
317
+ overlay?.actions.openOverlay(<DashboardConditionModal rowIndex={rowIdx} />)
318
+ }}
319
+ >
320
+ {iconHash['condition']}
321
+ </Button>
322
+ {row.dashboardCondition && (
323
+ <DashboardConditionSummary
324
+ className='dashboard-condition-summary--row'
325
+ dashboardCondition={row.dashboardCondition}
326
+ rowIndex={rowIdx}
327
+ />
328
+ )}
254
329
  <div className='column-container'>
255
330
  {row.columns
256
331
  .filter(column => column.width)