@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
@@ -2,6 +2,7 @@ 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'
@@ -10,13 +11,16 @@ import { AnyVisualization } from '@cdc/core/types/Visualization'
10
11
  import { iconHash } from '../../helpers/iconHash'
11
12
  import _ from 'lodash'
12
13
  import { DataDesignerModal } from '../DataDesignerModal'
14
+ import { DashboardConditionModal } from '../DashboardConditionModal'
15
+ import { DashboardConditionSummary } from '../DashboardConditionSummary'
13
16
  import { labelHash } from '@cdc/core/helpers/labelHash'
17
+ import { dashboardConditionsSupportedForRow } from '../../helpers/dashboardFilterTargets'
18
+ import { getConditionalWidgets, hasConditionalWidgets } from '../../helpers/dashboardColumnWidgets'
14
19
  import './widget.styles.css'
15
20
 
16
- type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number }
21
+ type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number; entryIdx?: number }
17
22
  type WidgetProps = {
18
23
  title: string
19
- columnData?: any
20
24
  widgetConfig?: WidgetConfig
21
25
  addVisualization?: Function
22
26
  type: string
@@ -26,7 +30,6 @@ type WidgetProps = {
26
30
 
27
31
  const Widget = ({
28
32
  title,
29
- columnData,
30
33
  widgetConfig,
31
34
  addVisualization,
32
35
  type,
@@ -35,11 +38,13 @@ const Widget = ({
35
38
  }: WidgetProps) => {
36
39
  const { overlay } = useGlobalContext()
37
40
  const { config, data, isEditor } = useContext(DashboardContext)
41
+ const { copiedWidget, copyWidget, clearCopiedWidget } = useContext(DashboardCopyPasteContext)
38
42
  const dispatch = useContext(DashboardDispatchContext)
43
+ const column = widgetConfig ? config.rows[widgetConfig.rowIdx]?.columns?.[widgetConfig.colIdx] : undefined
39
44
 
40
45
  const [isEditing, setIsEditing] = useState(false)
41
46
  const [toggleName, setToggleName] = useState(
42
- columnData?.toggleName || labelHash[config?.visualizations[columnData?.widget]?.type] || ''
47
+ column?.toggleName || labelHash[config?.visualizations[widgetConfig?.uid as string]?.type || type] || ''
43
48
  )
44
49
 
45
50
  const transform = new DataTransform()
@@ -49,14 +54,16 @@ const Widget = ({
49
54
 
50
55
  if (!result) return null
51
56
 
52
- const { rowIdx, colIdx } = result
57
+ const { rowIdx, colIdx, entryIdx } = result
53
58
 
54
59
  if (undefined !== widgetConfig?.rowIdx) {
55
- dispatch({ type: 'MOVE_VISUALIZATION', payload: { rowIdx, colIdx, widget: widgetConfig } })
60
+ dispatch({ type: 'MOVE_VISUALIZATION', payload: { rowIdx, colIdx, entryIdx, widget: widgetConfig } })
61
+ clearCopiedWidget()
56
62
  } else if (!!addVisualization) {
57
63
  // Item does not exist, instantiate a new one
58
64
  const newViz = addVisualization()
59
- dispatch({ type: 'ADD_VISUALIZATION', payload: { newViz, rowIdx, colIdx } })
65
+ dispatch({ type: 'ADD_VISUALIZATION', payload: { newViz, rowIdx, colIdx, entryIdx } })
66
+ clearCopiedWidget()
60
67
  }
61
68
  }
62
69
 
@@ -111,7 +118,7 @@ const Widget = ({
111
118
  // the HEADER component removes the data when you toggle to the main viz panel.
112
119
  // data will be cached only when it's loaded via dashboard preview.
113
120
  ;(responseData as any).sample = true
114
- dispatch({ type: 'SET_DATA', payload: { ...data, [dataKey]: responseData } })
121
+ dispatch({ type: 'SET_DATA', payload: { data: { ...data, [dataKey]: responseData } } })
115
122
  })
116
123
  .catch(error => {
117
124
  console.error('Failed to fetch sample data:', error)
@@ -129,6 +136,20 @@ const Widget = ({
129
136
  loadSampleData()
130
137
  }
131
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
+
132
153
  let isConfigurationReady = false
133
154
  const dataConfiguredForRow = !!config.rows[widgetConfig?.rowIdx]?.dataKey
134
155
  if (dataConfiguredForRow || ['dashboardFilters', 'markup-include'].includes(type)) {
@@ -157,25 +178,55 @@ const Widget = ({
157
178
  }
158
179
 
159
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
160
194
 
161
195
  const widgetContent = (
162
196
  <div
163
- 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'}`}
164
200
  style={{ maxHeight: widgetInRow && toggleRow ? '180px' : '180px', minHeight: '100%' }}
165
201
  >
166
202
  <Icon display='move' className='drag-icon' />
167
- <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
+ >
168
219
  {widgetConfig?.rowIdx !== undefined && (
169
220
  <div className='widget-menu'>
170
221
  {isConfigurationReady && (
171
- <Button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
222
+ <Button title='Configure Visualization' className='btn-configure' onClick={editWidget}>
172
223
  {iconHash['tools']}
173
224
  </Button>
174
225
  )}
175
226
  {needsDataConfiguration && (
176
227
  <Button
177
228
  title='Configure Data'
178
- className='btn btn-configure'
229
+ className='btn-configure'
179
230
  onClick={() => {
180
231
  overlay?.actions.openOverlay(
181
232
  <DataDesignerModal rowIndex={widgetConfig.rowIdx} vizKey={widgetConfig.uid} />
@@ -185,14 +236,65 @@ const Widget = ({
185
236
  {iconHash['gear']}
186
237
  </Button>
187
238
  )}
188
- <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}>
189
268
  <Icon display='close' base />
190
- </div>
269
+ </Button>
191
270
  </div>
192
271
  )}
193
- {iconHash[type]}
194
- <span>{labelHash[type]}</span>
195
- <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
+ )}
196
298
  {widgetConfig?.newViz && type !== 'dashboardFilters' && (
197
299
  <span onClick={editWidget} className='config-needed'>
198
300
  Configuration needed
@@ -42,12 +42,13 @@
42
42
  align-items: center;
43
43
  background: transparent !important;
44
44
  border: 0;
45
- border-radius: 0;
45
+ border-radius: 999px;
46
46
  box-shadow: none;
47
47
  color: var(--mediumGray);
48
48
  display: inline-flex;
49
49
  height: 28px;
50
50
  justify-content: center;
51
+ line-height: 1;
51
52
  margin: 0;
52
53
  min-height: 28px;
53
54
  padding: 4px;
@@ -57,32 +58,44 @@
57
58
 
58
59
  .widget-menu .cove-button.btn-configure:hover:not(:disabled),
59
60
  .widget-menu .cove-button.btn-configure:active:not(:disabled) {
60
- background: transparent !important;
61
+ background: rgba(0, 94, 170, 0.08) !important;
61
62
  box-shadow: none;
62
63
  transform: none;
63
64
  }
64
65
 
65
- .widget-menu .cove-button.btn-configure svg {
66
- margin-bottom: 0;
67
- top: 0;
68
- vertical-align: middle;
66
+ .widget-menu .cove-button.btn-configure:focus {
67
+ outline: none !important;
69
68
  }
70
69
 
71
- .widget-menu-item {
72
- align-items: center;
73
- cursor: pointer;
74
- display: inline-flex;
75
- height: 28px;
76
- justify-content: center;
77
- line-height: 1;
78
- padding: 4px;
79
- user-select: none;
80
- width: 28px;
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));
81
80
  }
82
81
 
83
- .widget-menu-item svg {
84
- fill: var(--mediumGray);
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));
86
+ }
87
+
88
+ .widget-menu .cove-button.btn-configure svg {
89
+ display: block;
90
+ height: 1.1em;
85
91
  margin-bottom: 0;
86
92
  top: 0;
93
+ transform: translateY(2px);
87
94
  vertical-align: middle;
95
+ width: 1.1em;
96
+ }
97
+
98
+ .widget-menu .cove-button.btn-configure.btn-configure--condition svg {
99
+ transform: translateY(2px) scale(1.15);
100
+ transform-origin: center;
88
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
+ }
@@ -0,0 +1,87 @@
1
+ .cove-visualization button.cove-button.dashboard-condition-summary {
2
+ align-items: center !important;
3
+ align-self: stretch;
4
+ background: var(--dashboard-condition-active-blue-bg, #f2f7fb) !important;
5
+ border: 1px solid var(--dashboard-condition-active-blue, var(--blue)) !important;
6
+ border-radius: 4px;
7
+ box-shadow: none;
8
+ box-sizing: border-box;
9
+ color: var(--dashboard-condition-active-blue, var(--blue)) !important;
10
+ display: inline-flex !important;
11
+ flex-direction: row !important;
12
+ flex-wrap: nowrap !important;
13
+ font-family: var(--app-font-main, sans-serif) !important;
14
+ font-size: 0.8rem !important;
15
+ font-weight: 400 !important;
16
+ gap: 0.35rem;
17
+ justify-content: flex-start;
18
+ line-height: 1.2;
19
+ margin: 0 0 0.5rem;
20
+ min-height: 24px !important;
21
+ overflow: visible;
22
+ padding: 0.2rem 0.45rem !important;
23
+ text-align: left;
24
+ transform: none;
25
+ white-space: normal;
26
+ width: 100%;
27
+ }
28
+
29
+ .cove-visualization button.cove-button.dashboard-condition-summary:hover,
30
+ .cove-visualization button.cove-button.dashboard-condition-summary:active,
31
+ .cove-visualization button.cove-button.dashboard-condition-summary:focus {
32
+ background: var(--dashboard-condition-active-blue-bg-hover, #e6eff7) !important;
33
+ box-shadow: none;
34
+ color: var(--dashboard-condition-active-blue, var(--blue)) !important;
35
+ transform: none;
36
+ }
37
+
38
+ .cove-visualization button.cove-button.dashboard-condition-summary .dashboard-condition-summary__icon {
39
+ align-items: center;
40
+ display: inline-flex;
41
+ flex: 0 0 auto;
42
+ line-height: 1.2;
43
+ margin-top: 0.05rem;
44
+ }
45
+
46
+ .cove-visualization button.cove-button.dashboard-condition-summary .dashboard-condition-summary__icon svg {
47
+ display: block;
48
+ fill: currentColor;
49
+ height: 1em;
50
+ margin: 0 !important;
51
+ position: static;
52
+ top: auto;
53
+ transform: none;
54
+ width: 1em;
55
+ }
56
+
57
+ .cove-visualization button.cove-button.dashboard-condition-summary .dashboard-condition-summary__text {
58
+ display: block;
59
+ flex: 1 1 auto;
60
+ font-family: var(--app-font-main, sans-serif) !important;
61
+ font-size: 0.8rem !important;
62
+ font-weight: 400 !important;
63
+ line-height: 1.2;
64
+ min-width: 0;
65
+ overflow-wrap: break-word;
66
+ white-space: normal;
67
+ word-break: normal;
68
+ }
69
+
70
+ .cove-visualization button.cove-button.dashboard-condition-summary .dashboard-condition-summary__text strong {
71
+ font-family: inherit;
72
+ font-size: inherit;
73
+ font-weight: 700;
74
+ line-height: inherit;
75
+ }
76
+
77
+ .cove-visualization button.cove-button.dashboard-condition-summary.dashboard-condition-summary--row {
78
+ margin-top: 0.5rem;
79
+ }
80
+
81
+ .cove-visualization button.cove-button.dashboard-condition-summary.dashboard-condition-summary--widget {
82
+ align-items: flex-start !important;
83
+ flex: 0 0 auto;
84
+ margin: 0.5rem 0 0.4rem;
85
+ max-width: 100%;
86
+ width: 100%;
87
+ }
@@ -58,11 +58,9 @@ export const addValuesToDashboardFilters = (
58
58
  const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
59
59
  filterCopy.active = active.filter(val => defaultValues.includes(val))
60
60
  } else {
61
- // Use defaultValue if set, otherwise keep existing active or use first value
62
- if (filterCopy.defaultValue) {
63
- filterCopy.active = filterCopy.defaultValue
64
- } else if (!filterCopy.active) {
65
- filterCopy.active = filterCopy.resetLabel || filterCopy.values[0]
61
+ // Only set active when it is not already set; prefer defaultValue, then resetLabel, then first value
62
+ if (!filterCopy.active) {
63
+ filterCopy.active = filterCopy.defaultValue || filterCopy.resetLabel || filterCopy.values[0]
66
64
  }
67
65
  }
68
66
  }
@@ -1,14 +1,22 @@
1
1
  import type { AnyVisualization } from '@cdc/core/types/Visualization'
2
2
  import type { Table } from '@cdc/core/types/Table'
3
+ import { createCoveId } from '@cdc/core/helpers/createCoveId'
4
+ import type { CreateCoveIdOptions } from '@cdc/core/helpers/createCoveId'
5
+
6
+ export const addVisualization = (type, subType, idOptions?: CreateCoveIdOptions) => {
7
+ if (type === 'filtered-text') {
8
+ throw new Error(
9
+ 'Cannot create new filtered-text visualizations. filtered-text is deprecated; use markup-include instead.'
10
+ )
11
+ }
3
12
 
4
- export const addVisualization = (type, subType) => {
5
13
  const modalWillOpen = type !== 'markup-include'
6
14
  const newVisualizationConfig: Partial<AnyVisualization> = {
7
15
  filters: [],
8
16
  filterBehavior: 'Filter Change',
9
17
  newViz: type !== 'table',
10
18
  openModal: modalWillOpen,
11
- uid: type + Date.now(),
19
+ uid: createCoveId(type, idOptions),
12
20
  type
13
21
  }
14
22
 
@@ -35,11 +43,11 @@ export const addVisualization = (type, subType) => {
35
43
  }
36
44
  break
37
45
  case 'data-bite':
38
- case 'filtered-text':
46
+ newVisualizationConfig.biteStyle = 'tp5'
39
47
  newVisualizationConfig.visualizationType = type
40
48
  break
41
49
  case 'waffle-chart':
42
- newVisualizationConfig.visualizationType = subType
50
+ newVisualizationConfig.visualizationType = subType === 'Waffle' ? 'TP5 Waffle' : subType
43
51
  break
44
52
  case 'table': {
45
53
  const tableConfig: Table = {
@@ -61,6 +69,9 @@ export const addVisualization = (type, subType) => {
61
69
  break
62
70
  case 'dashboardFilters': {
63
71
  newVisualizationConfig.sharedFilterIndexes = []
72
+ newVisualizationConfig.visual = {
73
+ grayBackground: false
74
+ }
64
75
  newVisualizationConfig.visualizationType = type
65
76
  break
66
77
  }
@@ -0,0 +1,127 @@
1
+ import _ from 'lodash'
2
+ import type { AnyVisualization } from '@cdc/core/types/Visualization'
3
+ import { createCoveId } from '@cdc/core/helpers/createCoveId'
4
+ import type { DashboardConfig } from '../types/DashboardConfig'
5
+ import { ConfigRow, DashboardCondition } from '../types/ConfigRow'
6
+ import { getDashboardConditionIds } from './dashboardConditions'
7
+ import { getConditionalWidgets, hasConditionalWidgets, normalizeConditionalColumn } from './dashboardColumnWidgets'
8
+
9
+ export type CloneDashboardWidgetTarget = {
10
+ rowIdx: number
11
+ colIdx: number
12
+ entryIdx?: number
13
+ }
14
+
15
+ const normalizeTarget = (target: string | number) => `${target}`
16
+
17
+ const appendTarget = (targets: (string | number)[], target: string | number) => {
18
+ if (targets.some(existingTarget => normalizeTarget(existingTarget) === normalizeTarget(target))) return targets
19
+ return [...targets, target]
20
+ }
21
+
22
+ const createClonedWidgetKey = (sourceWidgetKey: string, visualizations: Record<string, AnyVisualization>) => {
23
+ const sourceVisualization = visualizations[sourceWidgetKey]
24
+ return createCoveId(sourceVisualization.type, { existingIds: Object.keys(visualizations) })
25
+ }
26
+
27
+ const getSourceDashboardCondition = (rows: ConfigRow[], sourceWidgetKey: string): DashboardCondition | undefined => {
28
+ for (const row of rows) {
29
+ for (const column of row.columns || []) {
30
+ if (!hasConditionalWidgets(column)) continue
31
+
32
+ const sourceEntry = getConditionalWidgets(column).find(entry => entry.widget === sourceWidgetKey)
33
+ if (sourceEntry?.dashboardCondition) return sourceEntry.dashboardCondition
34
+ }
35
+ }
36
+
37
+ return undefined
38
+ }
39
+
40
+ const getWidgetFilterTarget = (rows: ConfigRow[], widgetKey: string): string | number => {
41
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
42
+ const row = rows[rowIndex]
43
+ const widgetInRow = row.columns?.some(
44
+ column => column.widget === widgetKey || getConditionalWidgets(column).some(entry => entry.widget === widgetKey)
45
+ )
46
+
47
+ if (widgetInRow) {
48
+ return row.dataKey ? rowIndex : widgetKey
49
+ }
50
+ }
51
+
52
+ return widgetKey
53
+ }
54
+
55
+ export const cloneDashboardWidget = (
56
+ config: DashboardConfig,
57
+ sourceWidgetKey: string,
58
+ target: CloneDashboardWidgetTarget
59
+ ): DashboardConfig => {
60
+ const sourceVisualization = config.visualizations?.[sourceWidgetKey]
61
+ const targetColumn = config.rows?.[target.rowIdx]?.columns?.[target.colIdx]
62
+
63
+ if (!sourceVisualization || !targetColumn) return config
64
+
65
+ if (target.entryIdx !== undefined) {
66
+ const existingEntry = getConditionalWidgets(targetColumn)[target.entryIdx]
67
+ if (existingEntry?.widget) return config
68
+ } else if (targetColumn.widget || hasConditionalWidgets(targetColumn)) {
69
+ return config
70
+ }
71
+
72
+ const clonedWidgetKey = createClonedWidgetKey(sourceWidgetKey, config.visualizations)
73
+ const clonedVisualization = { ..._.cloneDeep(sourceVisualization), uid: clonedWidgetKey }
74
+ const sourceDashboardCondition = getSourceDashboardCondition(config.rows, sourceWidgetKey)
75
+ const clonedDashboardCondition = sourceDashboardCondition
76
+ ? {
77
+ ..._.cloneDeep(sourceDashboardCondition),
78
+ id: createCoveId('condition', { existingIds: getDashboardConditionIds(config.rows) })
79
+ }
80
+ : undefined
81
+
82
+ const nextRows = _.cloneDeep(config.rows)
83
+ const nextTargetColumn = nextRows[target.rowIdx].columns[target.colIdx]
84
+
85
+ if (target.entryIdx !== undefined || hasConditionalWidgets(nextTargetColumn) || clonedDashboardCondition) {
86
+ const nextConditionalWidgets = [...(nextTargetColumn.conditionalWidgets || [])]
87
+ const targetEntryIndex = target.entryIdx ?? nextConditionalWidgets.length
88
+ nextConditionalWidgets[targetEntryIndex] = {
89
+ widget: clonedWidgetKey,
90
+ ...(clonedDashboardCondition ? { dashboardCondition: clonedDashboardCondition } : {})
91
+ }
92
+ nextRows[target.rowIdx].columns[target.colIdx] = normalizeConditionalColumn({
93
+ ...nextTargetColumn,
94
+ widget: undefined,
95
+ conditionalWidgets: nextConditionalWidgets.filter(entry => !!entry?.widget)
96
+ })
97
+ } else {
98
+ nextRows[target.rowIdx].columns[target.colIdx].widget = clonedWidgetKey
99
+ }
100
+
101
+ const sourceFilterTarget = getWidgetFilterTarget(config.rows, sourceWidgetKey)
102
+ const clonedFilterTarget = nextRows[target.rowIdx]?.dataKey ? target.rowIdx : clonedWidgetKey
103
+ const sharedFilters = config.dashboard.sharedFilters?.map(sharedFilter => {
104
+ if (!sharedFilter.usedBy?.length) return sharedFilter
105
+
106
+ let nextUsedBy = sharedFilter.usedBy
107
+
108
+ if (sharedFilter.usedBy.some(target => normalizeTarget(target) === normalizeTarget(sourceFilterTarget))) {
109
+ nextUsedBy = appendTarget(nextUsedBy, clonedFilterTarget)
110
+ }
111
+
112
+ return nextUsedBy === sharedFilter.usedBy ? sharedFilter : { ...sharedFilter, usedBy: nextUsedBy }
113
+ })
114
+
115
+ return {
116
+ ...config,
117
+ dashboard: {
118
+ ...config.dashboard,
119
+ ...(sharedFilters ? { sharedFilters } : {})
120
+ },
121
+ rows: nextRows,
122
+ visualizations: {
123
+ ...config.visualizations,
124
+ [clonedWidgetKey]: clonedVisualization
125
+ }
126
+ }
127
+ }