@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,218 @@
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 { DashboardCopyPasteContext } from '../../DashboardCopyPasteContext'
6
+ import { GlobalContext } from '@cdc/core/components/GlobalContext'
7
+ import Widget from './Widget'
8
+
9
+ vi.mock('@cdc/core/components/ui/Icon', () => ({
10
+ default: props => <span data-testid='mock-icon' {...props} />
11
+ }))
12
+
13
+ let latestDragSpec: any
14
+
15
+ vi.mock('react-dnd', () => ({
16
+ useDrag: spec => {
17
+ latestDragSpec = spec
18
+ return [{ isDragging: false }, () => {}]
19
+ }
20
+ }))
21
+
22
+ const renderWidget = (
23
+ options: {
24
+ copiedWidget?: any
25
+ copyWidget?: any
26
+ clearCopiedWidget?: any
27
+ dispatch?: any
28
+ type?: string
29
+ title?: string
30
+ widgetConfig?: any
31
+ } = {}
32
+ ) => {
33
+ const openOverlay = vi.fn()
34
+ const copyWidget = options.copyWidget || vi.fn()
35
+ const clearCopiedWidget = options.clearCopiedWidget || vi.fn()
36
+ const dispatch = options.dispatch || vi.fn()
37
+ const type = options.type || 'markup-include'
38
+ const title = options.title ?? 'Example'
39
+ const widgetConfig = options.widgetConfig || {
40
+ uid: 'markup-1',
41
+ rowIdx: 0,
42
+ colIdx: 0,
43
+ entryIdx: 0,
44
+ type: 'markup-include',
45
+ visualizationType: 'markup-include',
46
+ contentEditor: { title: 'Example' }
47
+ }
48
+
49
+ const renderResult = render(
50
+ <GlobalContext.Provider
51
+ value={{
52
+ overlay: {
53
+ object: null,
54
+ show: false,
55
+ disableBgClose: false,
56
+ actions: {
57
+ openOverlay,
58
+ toggleOverlay: vi.fn()
59
+ }
60
+ }
61
+ }}
62
+ >
63
+ <DashboardContext.Provider
64
+ value={{
65
+ ...initialState,
66
+ config: {
67
+ type: 'dashboard',
68
+ activeDashboard: 0,
69
+ dashboard: { sharedFilters: [] },
70
+ datasets: {},
71
+ rows: [
72
+ {
73
+ columns: [
74
+ {
75
+ width: 12,
76
+ conditionalWidgets: [
77
+ {
78
+ widget: 'markup-1',
79
+ dashboardCondition: {
80
+ id: 'column-condition-1',
81
+ datasetKey: 'condition-data',
82
+ operator: 'hasData'
83
+ }
84
+ }
85
+ ]
86
+ }
87
+ ],
88
+ expandCollapseAllButtons: false
89
+ }
90
+ ],
91
+ visualizations: {
92
+ 'markup-1': {
93
+ uid: 'markup-1',
94
+ type: 'markup-include',
95
+ visualizationType: 'markup-include',
96
+ contentEditor: { title: 'Example' }
97
+ }
98
+ }
99
+ } as any,
100
+ outerContainerRef: vi.fn(),
101
+ setParentConfig: vi.fn(),
102
+ isDebug: false,
103
+ isEditor: true,
104
+ reloadURLData: vi.fn(),
105
+ loadAPIFilters: vi.fn(),
106
+ setAPIFilterDropdowns: vi.fn(),
107
+ setAPILoading: vi.fn(),
108
+ data: {}
109
+ }}
110
+ >
111
+ <DashboardDispatchContext.Provider value={dispatch}>
112
+ <DashboardCopyPasteContext.Provider
113
+ value={{ copiedWidget: options.copiedWidget, copyWidget, clearCopiedWidget }}
114
+ >
115
+ <Widget title={title} toggleRow={false} type={type} widgetConfig={widgetConfig} widgetInRow />
116
+ </DashboardCopyPasteContext.Provider>
117
+ </DashboardDispatchContext.Provider>
118
+ </DashboardContext.Provider>
119
+ </GlobalContext.Provider>
120
+ )
121
+
122
+ return { ...renderResult, openOverlay, copyWidget, clearCopiedWidget, dispatch }
123
+ }
124
+
125
+ describe('Widget', () => {
126
+ it('shows the active widget condition button and summary strip when a column condition exists', () => {
127
+ renderWidget()
128
+
129
+ expect(screen.getByTitle('Configure Dashboard Condition')).toHaveClass('is-active')
130
+ expect(screen.getByRole('button', { name: "Configure Dashboard Condition: Show when there's data" })).toHaveClass(
131
+ 'dashboard-condition-summary'
132
+ )
133
+ })
134
+
135
+ it('opens the condition modal from the widget condition button or summary strip', () => {
136
+ const { openOverlay } = renderWidget()
137
+
138
+ fireEvent.click(screen.getByTitle('Configure Dashboard Condition'))
139
+ fireEvent.click(screen.getByRole('button', { name: "Configure Dashboard Condition: Show when there's data" }))
140
+
141
+ expect(openOverlay).toHaveBeenCalledTimes(2)
142
+ })
143
+
144
+ it('shows a copy button for populated widgets and stores the copied widget label', () => {
145
+ const { copyWidget } = renderWidget()
146
+
147
+ fireEvent.click(screen.getByTitle('Copy Component'))
148
+
149
+ expect(copyWidget).toHaveBeenCalledWith({ sourceWidgetKey: 'markup-1', label: 'Example' })
150
+ })
151
+
152
+ it('uses a delete button for removing widgets', () => {
153
+ const { dispatch } = renderWidget()
154
+
155
+ fireEvent.click(screen.getByRole('button', { name: 'Delete Component' }))
156
+
157
+ expect(dispatch).toHaveBeenCalledWith({
158
+ type: 'DELETE_WIDGET',
159
+ payload: { uid: 'markup-1' }
160
+ })
161
+ })
162
+
163
+ it('shows labels for waffle and gauge visualization type aliases', () => {
164
+ renderWidget({
165
+ type: 'TP5 Waffle',
166
+ title: '',
167
+ widgetConfig: {
168
+ uid: 'waffle-1',
169
+ rowIdx: 0,
170
+ colIdx: 0,
171
+ type: 'waffle-chart',
172
+ visualizationType: 'TP5 Waffle'
173
+ }
174
+ })
175
+
176
+ expect(screen.getByText('Waffle Chart')).toBeInTheDocument()
177
+ })
178
+
179
+ it('marks the copied widget and lets the active copy button cancel copy mode', () => {
180
+ const clearCopiedWidget = vi.fn()
181
+ const { container, copyWidget } = renderWidget({
182
+ copiedWidget: { sourceWidgetKey: 'markup-1', label: 'Example' },
183
+ clearCopiedWidget
184
+ })
185
+
186
+ expect(container.querySelector('.widget')).toHaveClass('widget--copied-source')
187
+ expect(screen.getByTitle('Copy Component')).toHaveClass('is-active')
188
+ expect(screen.getByRole('button', { name: 'Clear copied component' })).toHaveTextContent('Copied')
189
+
190
+ fireEvent.click(screen.getByRole('button', { name: 'Clear copied component' }))
191
+
192
+ expect(clearCopiedWidget).toHaveBeenCalledTimes(1)
193
+
194
+ fireEvent.click(screen.getByTitle('Copy Component'))
195
+
196
+ expect(clearCopiedWidget).toHaveBeenCalledTimes(2)
197
+ expect(copyWidget).not.toHaveBeenCalled()
198
+ })
199
+
200
+ it('clears copy mode after a successful widget move', () => {
201
+ const clearCopiedWidget = vi.fn()
202
+ const dispatch = vi.fn()
203
+ renderWidget({ clearCopiedWidget, dispatch })
204
+
205
+ latestDragSpec.end(null, { getDropResult: () => ({ rowIdx: 0, colIdx: 0 }) })
206
+
207
+ expect(dispatch).toHaveBeenCalledWith({
208
+ type: 'MOVE_VISUALIZATION',
209
+ payload: {
210
+ rowIdx: 0,
211
+ colIdx: 0,
212
+ entryIdx: undefined,
213
+ widget: expect.objectContaining({ uid: 'markup-1' })
214
+ }
215
+ })
216
+ expect(clearCopiedWidget).toHaveBeenCalled()
217
+ })
218
+ })
@@ -2,20 +2,25 @@ import React, { useContext, useState } from 'react'
2
2
  import { useDrag } from 'react-dnd'
3
3
  import { useGlobalContext } from '@cdc/core/components/GlobalContext'
4
4
  import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
5
+ import { DashboardCopyPasteContext } from '../../DashboardCopyPasteContext'
5
6
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
6
7
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
7
8
  import Icon from '@cdc/core/components/ui/Icon'
9
+ import Button from '@cdc/core/components/elements/Button'
8
10
  import { AnyVisualization } from '@cdc/core/types/Visualization'
9
11
  import { iconHash } from '../../helpers/iconHash'
10
12
  import _ from 'lodash'
11
13
  import { DataDesignerModal } from '../DataDesignerModal'
14
+ import { DashboardConditionModal } from '../DashboardConditionModal'
15
+ import { DashboardConditionSummary } from '../DashboardConditionSummary'
12
16
  import { labelHash } from '@cdc/core/helpers/labelHash'
17
+ import { dashboardConditionsSupportedForRow } from '../../helpers/dashboardFilterTargets'
18
+ import { getConditionalWidgets, hasConditionalWidgets } from '../../helpers/dashboardColumnWidgets'
13
19
  import './widget.styles.css'
14
20
 
15
- type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number }
21
+ type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number; entryIdx?: number }
16
22
  type WidgetProps = {
17
23
  title: string
18
- columnData?: any
19
24
  widgetConfig?: WidgetConfig
20
25
  addVisualization?: Function
21
26
  type: string
@@ -25,7 +30,6 @@ type WidgetProps = {
25
30
 
26
31
  const Widget = ({
27
32
  title,
28
- columnData,
29
33
  widgetConfig,
30
34
  addVisualization,
31
35
  type,
@@ -34,11 +38,13 @@ const Widget = ({
34
38
  }: WidgetProps) => {
35
39
  const { overlay } = useGlobalContext()
36
40
  const { config, data, isEditor } = useContext(DashboardContext)
41
+ const { copiedWidget, copyWidget, clearCopiedWidget } = useContext(DashboardCopyPasteContext)
37
42
  const dispatch = useContext(DashboardDispatchContext)
43
+ const column = widgetConfig ? config.rows[widgetConfig.rowIdx]?.columns?.[widgetConfig.colIdx] : undefined
38
44
 
39
45
  const [isEditing, setIsEditing] = useState(false)
40
46
  const [toggleName, setToggleName] = useState(
41
- columnData?.toggleName || labelHash[config?.visualizations[columnData?.widget]?.type] || ''
47
+ column?.toggleName || labelHash[config?.visualizations[widgetConfig?.uid as string]?.type || type] || ''
42
48
  )
43
49
 
44
50
  const transform = new DataTransform()
@@ -48,14 +54,16 @@ const Widget = ({
48
54
 
49
55
  if (!result) return null
50
56
 
51
- const { rowIdx, colIdx } = result
57
+ const { rowIdx, colIdx, entryIdx } = result
52
58
 
53
59
  if (undefined !== widgetConfig?.rowIdx) {
54
- dispatch({ type: 'MOVE_VISUALIZATION', payload: { rowIdx, colIdx, widget: widgetConfig } })
60
+ dispatch({ type: 'MOVE_VISUALIZATION', payload: { rowIdx, colIdx, entryIdx, widget: widgetConfig } })
61
+ clearCopiedWidget()
55
62
  } else if (!!addVisualization) {
56
63
  // Item does not exist, instantiate a new one
57
64
  const newViz = addVisualization()
58
- dispatch({ type: 'ADD_VISUALIZATION', payload: { newViz, rowIdx, colIdx } })
65
+ dispatch({ type: 'ADD_VISUALIZATION', payload: { newViz, rowIdx, colIdx, entryIdx } })
66
+ clearCopiedWidget()
59
67
  }
60
68
  }
61
69
 
@@ -110,7 +118,7 @@ const Widget = ({
110
118
  // the HEADER component removes the data when you toggle to the main viz panel.
111
119
  // data will be cached only when it's loaded via dashboard preview.
112
120
  ;(responseData as any).sample = true
113
- dispatch({ type: 'SET_DATA', payload: { ...data, [dataKey]: responseData } })
121
+ dispatch({ type: 'SET_DATA', payload: { data: { ...data, [dataKey]: responseData } } })
114
122
  })
115
123
  .catch(error => {
116
124
  console.error('Failed to fetch sample data:', error)
@@ -128,6 +136,20 @@ const Widget = ({
128
136
  loadSampleData()
129
137
  }
130
138
 
139
+ const copyCurrentWidget = () => {
140
+ if (!widgetConfig?.uid) return
141
+
142
+ if (copiedWidget?.sourceWidgetKey === widgetConfig.uid) {
143
+ clearCopiedWidget()
144
+ return
145
+ }
146
+
147
+ copyWidget({
148
+ sourceWidgetKey: widgetConfig.uid as string,
149
+ label: title || labelHash[type] || type
150
+ })
151
+ }
152
+
131
153
  let isConfigurationReady = false
132
154
  const dataConfiguredForRow = !!config.rows[widgetConfig?.rowIdx]?.dataKey
133
155
  if (dataConfiguredForRow || ['dashboardFilters', 'markup-include'].includes(type)) {
@@ -156,25 +178,55 @@ const Widget = ({
156
178
  }
157
179
 
158
180
  const needsDataConfiguration = !dataConfiguredForRow && widgetConfig?.type !== 'dashboardFilters'
181
+ const rowSupportsDashboardConditions = widgetConfig
182
+ ? dashboardConditionsSupportedForRow(config.rows[widgetConfig.rowIdx])
183
+ : false
184
+ const conditionalWidgets = hasConditionalWidgets(column) ? getConditionalWidgets(column) : []
185
+ const hasDashboardCondition =
186
+ widgetConfig && widgetConfig.entryIdx !== undefined
187
+ ? !!conditionalWidgets[widgetConfig.entryIdx]?.dashboardCondition
188
+ : false
189
+ const dashboardCondition =
190
+ widgetConfig && widgetConfig.entryIdx !== undefined
191
+ ? conditionalWidgets[widgetConfig.entryIdx]?.dashboardCondition
192
+ : undefined
193
+ const isCopiedWidget = !!widgetConfig?.uid && copiedWidget?.sourceWidgetKey === widgetConfig.uid
159
194
 
160
195
  const widgetContent = (
161
196
  <div
162
- className={`widget ${toggleRow ? 'd-block widget--toggle' : ''} ${isDragging && 'dragging'}`}
197
+ className={`widget ${toggleRow ? 'd-block widget--toggle' : ''} ${widgetInRow ? 'widget--in-row' : ''} ${
198
+ isCopiedWidget ? 'widget--copied-source' : ''
199
+ } ${isDragging && 'dragging'}`}
163
200
  style={{ maxHeight: widgetInRow && toggleRow ? '180px' : '180px', minHeight: '100%' }}
164
201
  >
165
202
  <Icon display='move' className='drag-icon' />
166
- <div className='widget__content'>
203
+ {isCopiedWidget && (
204
+ <button
205
+ type='button'
206
+ className='widget__copied-badge'
207
+ aria-label='Clear copied component'
208
+ onClick={clearCopiedWidget}
209
+ >
210
+ <span>Copied</span>
211
+ <Icon display='close' base />
212
+ </button>
213
+ )}
214
+ <div
215
+ className={`widget__content${widgetConfig?.rowIdx !== undefined ? ' widget__content--with-menu' : ''}${
216
+ dashboardCondition ? ' widget__content--has-condition' : ''
217
+ }`}
218
+ >
167
219
  {widgetConfig?.rowIdx !== undefined && (
168
220
  <div className='widget-menu'>
169
221
  {isConfigurationReady && (
170
- <button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
222
+ <Button title='Configure Visualization' className='btn-configure' onClick={editWidget}>
171
223
  {iconHash['tools']}
172
- </button>
224
+ </Button>
173
225
  )}
174
226
  {needsDataConfiguration && (
175
- <button
227
+ <Button
176
228
  title='Configure Data'
177
- className='btn btn-configure'
229
+ className='btn-configure'
178
230
  onClick={() => {
179
231
  overlay?.actions.openOverlay(
180
232
  <DataDesignerModal rowIndex={widgetConfig.rowIdx} vizKey={widgetConfig.uid} />
@@ -182,16 +234,67 @@ const Widget = ({
182
234
  }}
183
235
  >
184
236
  {iconHash['gear']}
185
- </button>
237
+ </Button>
186
238
  )}
187
- <div className='widget-menu-item' onClick={deleteWidget}>
239
+ <Button
240
+ title={
241
+ rowSupportsDashboardConditions
242
+ ? 'Configure Dashboard Condition'
243
+ : 'Dashboard conditions are not available for toggle or multi-visualization rows'
244
+ }
245
+ className={`btn-configure btn-configure--condition${hasDashboardCondition ? ' is-active' : ''}`}
246
+ disabled={!rowSupportsDashboardConditions}
247
+ onClick={() => {
248
+ overlay?.actions.openOverlay(
249
+ <DashboardConditionModal
250
+ rowIndex={widgetConfig.rowIdx}
251
+ columnIndex={widgetConfig.colIdx}
252
+ entryIndex={widgetConfig.entryIdx}
253
+ />
254
+ )
255
+ }}
256
+ >
257
+ {iconHash['condition']}
258
+ </Button>
259
+ <Button
260
+ title='Copy Component'
261
+ className={`btn-configure btn-configure--copy${isCopiedWidget ? ' is-active' : ''}`}
262
+ aria-pressed={isCopiedWidget}
263
+ onClick={copyCurrentWidget}
264
+ >
265
+ <Icon display='copy' base />
266
+ </Button>
267
+ <Button className='btn-configure btn-configure--delete' title='Delete Component' onClick={deleteWidget}>
188
268
  <Icon display='close' base />
189
- </div>
269
+ </Button>
190
270
  </div>
191
271
  )}
192
- {iconHash[type]}
193
- <span>{labelHash[type]}</span>
194
- <span>{title}</span>
272
+ {widgetConfig?.rowIdx !== undefined && dashboardCondition && (
273
+ <DashboardConditionSummary
274
+ className='dashboard-condition-summary--widget'
275
+ dashboardCondition={dashboardCondition}
276
+ rowIndex={widgetConfig.rowIdx}
277
+ columnIndex={widgetConfig.colIdx}
278
+ entryIndex={widgetConfig.entryIdx}
279
+ />
280
+ )}
281
+ {widgetInRow ? (
282
+ <>
283
+ <div className='widget__summary'>
284
+ <span className='widget__type-icon'>{iconHash[type]}</span>
285
+ <span className='widget__summary-text'>
286
+ <span className='widget__type-label'>{labelHash[type]}</span>
287
+ </span>
288
+ </div>
289
+ <span className='widget__title'>{title}</span>
290
+ </>
291
+ ) : (
292
+ <>
293
+ {iconHash[type]}
294
+ <span>{labelHash[type]}</span>
295
+ <span>{title}</span>
296
+ </>
297
+ )}
195
298
  {widgetConfig?.newViz && type !== 'dashboardFilters' && (
196
299
  <span onClick={editWidget} className='config-needed'>
197
300
  Configuration needed
@@ -33,25 +33,69 @@
33
33
  .widget-menu {
34
34
  align-items: center;
35
35
  display: flex;
36
- justify-content: space-between;
36
+ gap: 6px;
37
+ justify-content: flex-end;
38
+ padding: 2px;
37
39
  }
38
40
 
39
- .btn-configure {
40
- background: none;
41
- height: 20px;
42
- margin: 0 5px;
43
- padding: 0;
44
- width: 20px;
41
+ .widget-menu .cove-button.btn-configure {
42
+ align-items: center;
43
+ background: transparent !important;
44
+ border: 0;
45
+ border-radius: 999px;
46
+ box-shadow: none;
47
+ color: var(--mediumGray);
48
+ display: inline-flex;
49
+ height: 28px;
50
+ justify-content: center;
51
+ line-height: 1;
52
+ margin: 0;
53
+ min-height: 28px;
54
+ padding: 4px;
55
+ transform: none;
56
+ width: 28px;
57
+ }
58
+
59
+ .widget-menu .cove-button.btn-configure:hover:not(:disabled),
60
+ .widget-menu .cove-button.btn-configure:active:not(:disabled) {
61
+ background: rgba(0, 94, 170, 0.08) !important;
62
+ box-shadow: none;
63
+ transform: none;
64
+ }
65
+
66
+ .widget-menu .cove-button.btn-configure:focus {
67
+ outline: none !important;
68
+ }
69
+
70
+ .widget-menu .cove-button.btn-configure:focus-visible {
71
+ background: rgba(0, 94, 170, 0.08) !important;
72
+ box-shadow: inset 0 0 0 2px currentColor;
73
+ outline: none !important;
74
+ }
75
+
76
+ .widget-menu .cove-button.btn-configure.btn-configure--condition.is-active {
77
+ background: var(--dashboard-condition-active-blue-bg, #f2f7fb) !important;
78
+ box-shadow: inset 0 0 0 1px var(--dashboard-condition-active-blue, var(--blue));
79
+ color: var(--dashboard-condition-active-blue, var(--blue));
80
+ }
81
+
82
+ .widget-menu .cove-button.btn-configure.btn-configure--condition.is-active:hover:not(:disabled),
83
+ .widget-menu .cove-button.btn-configure.btn-configure--condition.is-active:active:not(:disabled) {
84
+ background: var(--dashboard-condition-active-blue-bg-hover, #e6eff7) !important;
85
+ box-shadow: inset 0 0 0 1px var(--dashboard-condition-active-blue, var(--blue));
45
86
  }
46
87
 
47
- .widget-menu-item {
48
- cursor: pointer;
88
+ .widget-menu .cove-button.btn-configure svg {
49
89
  display: block;
50
- height: 20px;
51
- user-select: none;
52
- width: 20px;
90
+ height: 1.1em;
91
+ margin-bottom: 0;
92
+ top: 0;
93
+ transform: translateY(2px);
94
+ vertical-align: middle;
95
+ width: 1.1em;
53
96
  }
54
97
 
55
- .widget-menu-item svg {
56
- fill: var(--mediumGray);
98
+ .widget-menu .cove-button.btn-configure.btn-configure--condition svg {
99
+ transform: translateY(2px) scale(1.15);
100
+ transform-origin: center;
57
101
  }
@@ -0,0 +1,76 @@
1
+ .cove-overlay:has(.dashboard-condition-modal) {
2
+ align-items: flex-start;
3
+ }
4
+
5
+ .cove-overlay__wrapper:has(.dashboard-condition-modal) {
6
+ align-items: flex-start;
7
+ padding: 2.5rem 0;
8
+ }
9
+
10
+ .cove-overlay__container:has(.dashboard-condition-modal) {
11
+ margin: 0 auto !important;
12
+ }
13
+
14
+ .dashboard-condition-modal__fields {
15
+ display: grid;
16
+ gap: 1rem;
17
+ }
18
+
19
+ .dashboard-condition-modal__select {
20
+ min-height: 41px;
21
+ }
22
+
23
+ .dashboard-condition-modal label {
24
+ align-items: flex-start;
25
+ color: var(--text-primary);
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+
30
+ .dashboard-condition-modal .edit-label {
31
+ align-items: center;
32
+ color: #333;
33
+ display: inline-flex;
34
+ font-size: 0.95rem;
35
+ font-weight: 600;
36
+ gap: 0.125rem;
37
+ margin-bottom: 0.5rem;
38
+ width: 100%;
39
+ }
40
+
41
+ .dashboard-condition-modal .cove-form-select {
42
+ border: 1px solid #c7c7c7;
43
+ border-radius: 0.333rem;
44
+ display: block;
45
+ height: 41px;
46
+ margin-top: 0;
47
+ min-height: 41px;
48
+ padding-left: 0.875rem;
49
+ padding-right: 2.5rem;
50
+ transition: border-color 150ms ease, box-shadow 150ms ease;
51
+ width: 100%;
52
+ }
53
+
54
+ .dashboard-condition-modal .cove-form-select:hover {
55
+ border-color: var(--blue);
56
+ }
57
+
58
+ .dashboard-condition-modal .cove-form-select:focus {
59
+ border-color: var(--blue);
60
+ box-shadow: none;
61
+ outline: 0;
62
+ }
63
+
64
+ .dashboard-condition-modal .cove-multiselect {
65
+ margin-top: 0.25rem;
66
+ }
67
+
68
+ .dashboard-condition-modal__multiselect-field {
69
+ display: flex;
70
+ flex-direction: column;
71
+ }
72
+
73
+ .dashboard-condition-modal__tooltip-text {
74
+ font-weight: 400;
75
+ margin: 0;
76
+ }