@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.
- package/CONFIG.md +77 -30
- package/LICENSE +201 -0
- package/dist/cdcdashboard.js +49936 -49166
- package/examples/dashboard-conditions-filters-incomplete.json +221 -0
- package/examples/dashboard-missing-datasets-multi.json +174 -0
- package/examples/dashboard-missing-datasets-single.json +121 -0
- package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
- package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
- package/examples/dashboard-stale-dataset-keys.json +181 -0
- package/examples/dashboard-tiered-filter-regression.json +190 -0
- package/examples/private/cfa-dashboard.json +651 -0
- package/examples/private/data-bite-wrap.json +6936 -0
- package/examples/private/multi-dash-fix.json +16963 -0
- package/examples/private/versions.json +41612 -0
- package/examples/us-map-filter-example.json +1074 -0
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +6 -2
- package/src/CdcDashboardComponent.tsx +178 -87
- package/src/DashboardCopyPasteContext.test.tsx +33 -0
- package/src/DashboardCopyPasteContext.tsx +48 -0
- package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
- package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
- package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
- package/src/_stories/Dashboard.stories.tsx +294 -0
- package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
- package/src/components/Column.test.tsx +176 -0
- package/src/components/Column.tsx +214 -13
- package/src/components/DashboardConditionModal.test.tsx +420 -0
- package/src/components/DashboardConditionModal.tsx +367 -0
- package/src/components/DashboardConditionSummary.tsx +59 -0
- package/src/components/DashboardEditors.tsx +8 -0
- package/src/components/DashboardFilters/DashboardFilters.test.tsx +139 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +192 -174
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +41 -2
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +180 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +15 -32
- package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
- package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
- package/src/components/DataDesignerModal.tsx +2 -1
- package/src/components/Grid.tsx +8 -4
- package/src/components/Header/Header.tsx +36 -17
- package/src/components/Row.test.tsx +228 -0
- package/src/components/Row.tsx +93 -18
- package/src/components/VisualizationRow.test.tsx +396 -0
- package/src/components/VisualizationRow.tsx +110 -35
- package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
- package/src/components/Widget/Widget.test.tsx +218 -0
- package/src/components/Widget/Widget.tsx +119 -17
- package/src/components/Widget/widget.styles.css +31 -18
- package/src/components/dashboard-condition-modal.css +76 -0
- package/src/components/dashboard-condition-summary.css +87 -0
- package/src/helpers/addValuesToDashboardFilters.ts +3 -5
- package/src/helpers/addVisualization.ts +15 -4
- package/src/helpers/cloneDashboardWidget.ts +127 -0
- package/src/helpers/dashboardColumnWidgets.ts +99 -0
- package/src/helpers/dashboardConditionUi.ts +47 -0
- package/src/helpers/dashboardConditions.ts +200 -0
- package/src/helpers/dashboardFilterTargets.ts +156 -0
- package/src/helpers/filterData.ts +4 -9
- package/src/helpers/filterVisibility.ts +20 -0
- package/src/helpers/formatConfigBeforeSave.ts +2 -2
- package/src/helpers/getFilteredData.ts +18 -5
- package/src/helpers/getUpdateConfig.ts +43 -12
- package/src/helpers/getVizRowColumnLocator.ts +11 -1
- package/src/helpers/iconHash.tsx +9 -3
- package/src/helpers/mapDataToConfig.ts +31 -29
- package/src/helpers/reloadURLHelpers.ts +25 -5
- package/src/helpers/removeDashboardFilter.ts +33 -33
- package/src/helpers/tests/addVisualization.test.ts +53 -9
- package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
- package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
- package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
- package/src/helpers/tests/dashboardConditions.test.ts +428 -0
- package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
- package/src/helpers/tests/getFilteredData.test.ts +265 -86
- package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
- package/src/index.tsx +6 -3
- package/src/scss/grid.scss +249 -20
- package/src/scss/main.scss +108 -29
- package/src/store/dashboard.actions.ts +17 -4
- package/src/store/dashboard.reducer.test.ts +538 -0
- package/src/store/dashboard.reducer.ts +135 -22
- package/src/test/CdcDashboard.test.tsx +148 -0
- package/src/test/CdcDashboardComponent.test.tsx +935 -2
- package/src/types/ConfigRow.ts +15 -0
- package/src/types/DashboardFilters.ts +4 -0
- 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
|
-
|
|
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' : ''} ${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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
|
-
</
|
|
269
|
+
</Button>
|
|
191
270
|
</div>
|
|
192
271
|
)}
|
|
193
|
-
{
|
|
194
|
-
|
|
195
|
-
|
|
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:
|
|
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:
|
|
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
|
|
66
|
-
|
|
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-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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-
|
|
84
|
-
|
|
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
|
-
//
|
|
62
|
-
if (filterCopy.
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|