@cdc/dashboard 4.26.3 → 4.26.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CONFIG.md +219 -0
  2. package/README.md +60 -20
  3. package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcdashboard.js +61559 -58048
  6. package/examples/__data__/data-2.json +6 -0
  7. package/examples/__data__/data.json +6 -0
  8. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  9. package/examples/dashboard-missing-datasets-multi.json +174 -0
  10. package/examples/dashboard-missing-datasets-single.json +121 -0
  11. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  12. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  13. package/examples/dashboard-stale-dataset-keys.json +181 -0
  14. package/examples/dashboard-tiered-filter-regression.json +190 -0
  15. package/examples/legend-issue.json +1 -1
  16. package/examples/minimal-example.json +34 -0
  17. package/examples/private/cfa-dashboard.json +651 -0
  18. package/examples/private/data-bite-wrap.json +6936 -0
  19. package/examples/private/dengue.json +4640 -0
  20. package/examples/private/link_to_file.json +16662 -0
  21. package/examples/private/multi-dash-fix.json +16963 -0
  22. package/examples/private/versions.json +41612 -0
  23. package/examples/sankey.json +3 -3
  24. package/examples/test-api-filter-reset.json +4 -4
  25. package/examples/tp5-test.json +86 -4
  26. package/examples/us-map-filter-example.json +1074 -0
  27. package/package.json +9 -9
  28. package/src/CdcDashboard.tsx +6 -2
  29. package/src/CdcDashboardComponent.tsx +179 -88
  30. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  31. package/src/DashboardCopyPasteContext.tsx +48 -0
  32. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  33. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  34. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  35. package/src/_stories/Dashboard.smoke.stories.tsx +33 -0
  36. package/src/_stories/Dashboard.stories.tsx +337 -2
  37. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  38. package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
  39. package/src/_stories/_mock/tp5-test.json +86 -5
  40. package/src/components/Column.test.tsx +176 -0
  41. package/src/components/Column.tsx +214 -13
  42. package/src/components/DashboardConditionModal.test.tsx +420 -0
  43. package/src/components/DashboardConditionModal.tsx +367 -0
  44. package/src/components/DashboardConditionSummary.tsx +59 -0
  45. package/src/components/DashboardEditors.tsx +23 -0
  46. package/src/components/DashboardFilters/DashboardFilters.test.tsx +267 -0
  47. package/src/components/DashboardFilters/DashboardFilters.tsx +193 -172
  48. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  49. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +46 -6
  50. package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
  51. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
  52. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +304 -0
  53. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +43 -36
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
  55. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  56. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  57. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  58. package/src/components/DataDesignerModal.tsx +2 -1
  59. package/src/components/ExpandCollapseButtons.tsx +6 -4
  60. package/src/components/Grid.tsx +12 -7
  61. package/src/components/Header/Header.tsx +36 -17
  62. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
  63. package/src/components/Row.test.tsx +228 -0
  64. package/src/components/Row.tsx +104 -28
  65. package/src/components/VisualizationRow.test.tsx +396 -0
  66. package/src/components/VisualizationRow.tsx +177 -51
  67. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  68. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  69. package/src/components/Widget/Widget.test.tsx +218 -0
  70. package/src/components/Widget/Widget.tsx +123 -20
  71. package/src/components/Widget/widget.styles.css +58 -14
  72. package/src/components/dashboard-condition-modal.css +76 -0
  73. package/src/components/dashboard-condition-summary.css +87 -0
  74. package/src/data/initial-state.js +1 -0
  75. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  76. package/src/helpers/addVisualization.ts +17 -4
  77. package/src/helpers/cloneDashboardWidget.ts +127 -0
  78. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  79. package/src/helpers/dashboardConditionUi.ts +47 -0
  80. package/src/helpers/dashboardConditions.ts +200 -0
  81. package/src/helpers/dashboardFilterTargets.ts +156 -0
  82. package/src/helpers/filterData.ts +4 -9
  83. package/src/helpers/filterVisibility.ts +20 -0
  84. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  85. package/src/helpers/getFilteredData.ts +18 -5
  86. package/src/helpers/getUpdateConfig.ts +43 -12
  87. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  88. package/src/helpers/iconHash.tsx +9 -3
  89. package/src/helpers/mapDataToConfig.ts +31 -29
  90. package/src/helpers/reloadURLHelpers.ts +25 -5
  91. package/src/helpers/removeDashboardFilter.ts +33 -33
  92. package/src/helpers/tests/addVisualization.test.ts +53 -9
  93. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  94. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  95. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  96. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  97. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  98. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  99. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  100. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  101. package/src/index.tsx +6 -3
  102. package/src/scss/grid.scss +281 -22
  103. package/src/scss/main.scss +215 -64
  104. package/src/store/dashboard.actions.ts +17 -4
  105. package/src/store/dashboard.reducer.test.ts +538 -0
  106. package/src/store/dashboard.reducer.ts +136 -22
  107. package/src/test/CdcDashboard.test.jsx +24 -0
  108. package/src/test/CdcDashboard.test.tsx +148 -0
  109. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  110. package/src/types/ConfigRow.ts +15 -0
  111. package/src/types/DashboardFilters.ts +4 -0
  112. package/src/types/SharedFilter.ts +2 -0
  113. package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
  114. package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
  115. package/examples/DEV-6574.json +0 -2224
  116. package/examples/api-dashboard-data.json +0 -272
  117. package/examples/api-dashboard-years.json +0 -11
  118. package/examples/api-geographies-data.json +0 -11
  119. package/examples/chart-data.json +0 -5409
  120. package/examples/custom/css/respiratory.css +0 -236
  121. package/examples/custom/js/respiratory.js +0 -242
  122. package/examples/default-data.json +0 -368
  123. package/examples/default-filter-control.json +0 -209
  124. package/examples/default-multi-dataset-shared-filter.json +0 -1729
  125. package/examples/default-multi-dataset.json +0 -506
  126. package/examples/ed-visits-county-file.json +0 -402
  127. package/examples/filters/Alabama.json +0 -72
  128. package/examples/filters/Alaska.json +0 -1737
  129. package/examples/filters/Arkansas.json +0 -4713
  130. package/examples/filters/California.json +0 -212
  131. package/examples/filters/Colorado.json +0 -1500
  132. package/examples/filters/Connecticut.json +0 -559
  133. package/examples/filters/Delaware.json +0 -63
  134. package/examples/filters/DistrictofColumbia.json +0 -63
  135. package/examples/filters/Florida.json +0 -4217
  136. package/examples/filters/States.json +0 -146
  137. package/examples/state-level.json +0 -90136
  138. package/examples/state-points.json +0 -10474
  139. package/examples/temp-example-data.json +0 -130
  140. package/examples/test-dashboard-simple.json +0 -503
  141. package/examples/test-example.json +0 -752
  142. package/examples/test-file.json +0 -147
  143. package/examples/test.json +0 -752
  144. package/examples/testing.json +0 -94456
  145. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  146. /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
  147. /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
  148. /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
  149. /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
  150. /package/examples/api-test/{years.json → __data__/years.json} +0 -0
  151. /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
@@ -0,0 +1,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,11 +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'
20
+ import Button from '@cdc/core/components/elements/Button'
18
21
  import { iconHash } from '../helpers/iconHash'
19
22
  import _ from 'lodash'
20
23
  import { Visualization } from '@cdc/core/types/Visualization'
21
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'
22
29
 
23
30
  type RowMenuProps = {
24
31
  rowIdx: number
@@ -45,7 +52,7 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
45
52
  const newRows = _.cloneDeep(rows)
46
53
  newRows[rowIdx].toggle = toggle
47
54
  const rowColumns = newRows[rowIdx].columns
48
- const columnsWithWidgets = rowColumns.filter(c => c.widget)
55
+ const columnsWithWidgets = rowColumns.filter(c => getColumnWidgetKeys(c).length > 0)
49
56
 
50
57
  const totalWidgets = columnsWithWidgets.length
51
58
  if (totalWidgets > layout.length) {
@@ -58,7 +65,8 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
58
65
 
59
66
  // Adds placeholder column name that defaults to the visualization type.
60
67
  newRows[rowIdx].columns.forEach((col, idx) => {
61
- 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
62
70
  })
63
71
 
64
72
  newRows[rowIdx].columns = layout.map((width, colIndex) => {
@@ -87,10 +95,20 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
87
95
  rows[newIdx] = row
88
96
  rows[rowIdx] = temp
89
97
 
90
- rows[newIdx].uuid = Date.now()
91
- 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] })
92
101
 
93
- 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 } })
94
112
 
95
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.
96
114
  let calcRowMove = dir === 'down' ? 202 : -202
@@ -99,6 +117,8 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
99
117
  let rowEle = document.querySelector("[data-row-id='" + rowIdx + "']") as HTMLElement
100
118
  let rowNewEle = document.querySelector("[data-row-id='" + newIdx + "']") as HTMLElement
101
119
 
120
+ if (!rowEle || !rowNewEle) return
121
+
102
122
  rowEle.style.pointerEvents = 'none'
103
123
  rowNewEle.style.pointerEvents = 'none'
104
124
  rowEle.style.top = calcRowMove + 'px'
@@ -119,19 +139,47 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
119
139
 
120
140
  const deleteRow = () => {
121
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
+ })
122
147
 
123
- //delete the instantiated widgets
124
- if (rows[rowIdx] && rows[rowIdx].columns && rows[rowIdx].columns.length && config.visualizations) {
148
+ if (rows[rowIdx]?.columns?.length && config.visualizations) {
125
149
  rows[rowIdx].columns.forEach(column => {
126
- if (column.widget) {
127
- delete newVisualizations[column.widget]
128
- }
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
+ })
129
158
  })
130
159
  }
131
160
 
132
- 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
+ }
133
176
 
134
- updateConfig({ ...config, rows, visualizations: newVisualizations })
177
+ updateConfig({
178
+ ...config,
179
+ rows,
180
+ visualizations: currentVizs,
181
+ dashboard: { ...config.dashboard, sharedFilters: currentFilters }
182
+ })
135
183
  }
136
184
 
137
185
  const layoutList = [
@@ -191,8 +239,9 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
191
239
  <nav className='row-menu'>
192
240
  <ul className='row-menu__flyout'>{layoutList}</ul>
193
241
  {isMultiColumn && (
194
- <button
195
- className={`btn row-menu__btn border-0${row.equalHeight ? ' btn-primary' : ''}`}
242
+ <Button
243
+ variant={row.equalHeight ? 'primary' : undefined}
244
+ className='row-menu__btn border-0'
196
245
  title={row.equalHeight ? 'Disable Equal Height Rows' : 'Enable Equal Height Rows'}
197
246
  onClick={toggleEqualHeight}
198
247
  >
@@ -201,33 +250,36 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
201
250
  <rect x='14' y='2' width='9' height='14' rx='1' />
202
251
  <line x1='0' y1='19' x2='24' y2='19' stroke='#fff' strokeWidth='2' strokeDasharray='3 2' />
203
252
  </svg>
204
- </button>
253
+ </Button>
205
254
  )}
206
255
  <div className='spacer'></div>
207
- <button
208
- className={`btn btn-primary row-menu__btn border-0`}
256
+ <Button
257
+ variant='primary'
258
+ className='row-menu__btn border-0'
209
259
  title='Move Row Up'
210
260
  onClick={() => moveRow('up')}
211
261
  disabled={rowIdx === 0}
212
262
  >
213
263
  <Icon display='caretUp' color='#fff' size={25} />
214
- </button>
215
- <button
216
- className={'btn btn-primary row-menu__btn border-0'}
264
+ </Button>
265
+ <Button
266
+ variant='primary'
267
+ className='row-menu__btn border-0'
217
268
  title='Move Row Down'
218
269
  onClick={() => moveRow('down')}
219
270
  disabled={rowIdx + 1 === rows.length}
220
271
  >
221
272
  <Icon display='caretDown' color='#fff' size={25} />
222
- </button>
223
- <button
224
- className={'btn btn-danger row-menu__btn row-menu__btn--remove border-0'}
273
+ </Button>
274
+ <Button
275
+ variant='danger'
276
+ className='row-menu__btn row-menu__btn--remove border-0'
225
277
  title='Delete Row'
226
278
  onClick={deleteRow}
227
279
  disabled={rowIdx === 0 && rows.length === 1}
228
280
  >
229
281
  <Icon display='close' color='#fff' size={25} />
230
- </button>
282
+ </Button>
231
283
  </nav>
232
284
  )
233
285
  }
@@ -236,20 +288,44 @@ type RowProps = { row: ConfigRow; idx: number; uuid: number | string }
236
288
 
237
289
  const Row: React.FC<RowProps> = ({ row, idx: rowIdx, uuid }) => {
238
290
  const { overlay } = useGlobalContext()
291
+ const supportsDashboardConditions = dashboardConditionsSupportedForRow(row)
292
+ const hasDashboardCondition = !!row.dashboardCondition
293
+
239
294
  return (
240
295
  <>
241
296
  <div className='builder-row' data-row-id={rowIdx}>
242
297
  <RowMenu rowIdx={rowIdx} />
243
- <span className='ms-2 mt-n3'>Row - {rowIdx + 1}</span>
244
- <button
298
+ <span className='builder-row__label'>Row {rowIdx + 1}</span>
299
+ <Button
245
300
  title='Configure Data'
246
- className='btn btn-configure-row'
301
+ className='btn-configure-row btn-configure-row--data'
247
302
  onClick={() => {
248
303
  overlay?.actions.openOverlay(<DataDesignerModal rowIndex={rowIdx} />)
249
304
  }}
250
305
  >
251
306
  {iconHash['gearMulti']}
252
- </button>
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
+ )}
253
329
  <div className='column-container'>
254
330
  {row.columns
255
331
  .filter(column => column.width)