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