@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
|
@@ -0,0 +1,176 @@
|
|
|
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 Column from './Column'
|
|
7
|
+
|
|
8
|
+
vi.mock('react-dnd', () => ({
|
|
9
|
+
useDrop: () => [{ isOver: false, canDrop: false }, vi.fn()]
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('./Widget/Widget', () => ({
|
|
13
|
+
default: ({ title }) => <div>{title}</div>
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const renderColumn = ({
|
|
17
|
+
data,
|
|
18
|
+
copiedWidget = undefined,
|
|
19
|
+
sharedFilters = [],
|
|
20
|
+
visualizations = {}
|
|
21
|
+
}: {
|
|
22
|
+
data: any
|
|
23
|
+
copiedWidget?: any
|
|
24
|
+
sharedFilters?: any[]
|
|
25
|
+
visualizations?: any
|
|
26
|
+
}) => {
|
|
27
|
+
const dispatch = vi.fn()
|
|
28
|
+
const clearCopiedWidget = vi.fn()
|
|
29
|
+
|
|
30
|
+
render(
|
|
31
|
+
<DashboardContext.Provider
|
|
32
|
+
value={
|
|
33
|
+
{
|
|
34
|
+
...initialState,
|
|
35
|
+
config: {
|
|
36
|
+
type: 'dashboard',
|
|
37
|
+
activeDashboard: 0,
|
|
38
|
+
dashboard: { sharedFilters },
|
|
39
|
+
datasets: {},
|
|
40
|
+
rows: [{ columns: [data], expandCollapseAllButtons: false }],
|
|
41
|
+
visualizations: {
|
|
42
|
+
'existing-widget': {
|
|
43
|
+
uid: 'existing-widget',
|
|
44
|
+
type: 'markup-include',
|
|
45
|
+
visualizationType: 'markup-include',
|
|
46
|
+
contentEditor: { title: 'Existing' }
|
|
47
|
+
},
|
|
48
|
+
...visualizations
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
outerContainerRef: vi.fn(),
|
|
52
|
+
setParentConfig: vi.fn(),
|
|
53
|
+
isDebug: false,
|
|
54
|
+
isEditor: true,
|
|
55
|
+
reloadURLData: vi.fn(),
|
|
56
|
+
loadAPIFilters: vi.fn(),
|
|
57
|
+
setAPIFilterDropdowns: vi.fn(),
|
|
58
|
+
setAPILoading: vi.fn(),
|
|
59
|
+
data: {}
|
|
60
|
+
} as any
|
|
61
|
+
}
|
|
62
|
+
>
|
|
63
|
+
<DashboardDispatchContext.Provider value={dispatch}>
|
|
64
|
+
<DashboardCopyPasteContext.Provider value={{ copiedWidget, copyWidget: vi.fn(), clearCopiedWidget }}>
|
|
65
|
+
<Column data={data} rowIdx={0} colIdx={0} toggleRow={false} />
|
|
66
|
+
</DashboardCopyPasteContext.Provider>
|
|
67
|
+
</DashboardDispatchContext.Provider>
|
|
68
|
+
</DashboardContext.Provider>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return { dispatch, clearCopiedWidget }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('Column copy paste slots', () => {
|
|
75
|
+
it('shows paste-ready text in an empty simple column and dispatches clone on click', () => {
|
|
76
|
+
const copiedWidget = { sourceWidgetKey: 'source-widget', label: 'Source' }
|
|
77
|
+
const { dispatch, clearCopiedWidget } = renderColumn({ data: { width: 12 }, copiedWidget })
|
|
78
|
+
|
|
79
|
+
fireEvent.click(
|
|
80
|
+
screen.getByRole('button', { name: 'Click here to paste copied component or drag and drop a new visualization' })
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
84
|
+
type: 'CLONE_VISUALIZATION',
|
|
85
|
+
payload: { sourceWidgetKey: 'source-widget', rowIdx: 0, colIdx: 0 }
|
|
86
|
+
})
|
|
87
|
+
expect(clearCopiedWidget).toHaveBeenCalled()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('shows paste-ready text in the empty conditional slot and dispatches with entry index', () => {
|
|
91
|
+
const copiedWidget = { sourceWidgetKey: 'source-widget', label: 'Source' }
|
|
92
|
+
const { dispatch } = renderColumn({
|
|
93
|
+
data: { width: 12, conditionalWidgets: [{ widget: 'existing-widget' }] },
|
|
94
|
+
copiedWidget
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
fireEvent.click(
|
|
98
|
+
screen.getByRole('button', {
|
|
99
|
+
name: 'Click here to paste copied alternate visualization or drag and drop a new alternate'
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
104
|
+
type: 'CLONE_VISUALIZATION',
|
|
105
|
+
payload: { sourceWidgetKey: 'source-widget', rowIdx: 0, colIdx: 0, entryIdx: 1 }
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('labels empty conditional slots as first-match alternates', () => {
|
|
110
|
+
renderColumn({
|
|
111
|
+
data: { width: 12, conditionalWidgets: [{ widget: 'existing-widget' }] }
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(screen.getByText('Drag and drop an alternate visualization.')).toBeInTheDocument()
|
|
115
|
+
expect(
|
|
116
|
+
screen.getByText('If multiple conditions match, only the first match in this column is shown.')
|
|
117
|
+
).toBeInTheDocument()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('Column widget summaries', () => {
|
|
122
|
+
it('shows dashboard filter labels from user-entered keys when they are configured', () => {
|
|
123
|
+
renderColumn({
|
|
124
|
+
data: { width: 12, widget: 'dashboard-filters' },
|
|
125
|
+
sharedFilters: [
|
|
126
|
+
{ key: 'State', columnName: 'state' },
|
|
127
|
+
{ key: 'Year', columnName: 'year' },
|
|
128
|
+
{ key: 'Topic', columnName: 'topic' },
|
|
129
|
+
{ key: 'Response', columnName: 'response' }
|
|
130
|
+
],
|
|
131
|
+
visualizations: {
|
|
132
|
+
'dashboard-filters': {
|
|
133
|
+
uid: 'dashboard-filters',
|
|
134
|
+
type: 'dashboardFilters',
|
|
135
|
+
visualizationType: 'dashboardFilters',
|
|
136
|
+
sharedFilterIndexes: [0, 1, 2, 3]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
expect(screen.getByText('State, Year, Topic, Response')).toBeInTheDocument()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('falls back to dashboard filter columns when labels are not configured', () => {
|
|
145
|
+
renderColumn({
|
|
146
|
+
data: { width: 12, widget: 'dashboard-filters' },
|
|
147
|
+
sharedFilters: [{ key: '', columnName: 'state' }, { columnName: 'year' }],
|
|
148
|
+
visualizations: {
|
|
149
|
+
'dashboard-filters': {
|
|
150
|
+
uid: 'dashboard-filters',
|
|
151
|
+
type: 'dashboardFilters',
|
|
152
|
+
visualizationType: 'dashboardFilters',
|
|
153
|
+
sharedFilterIndexes: [0, 1]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
expect(screen.getByText('state, year')).toBeInTheDocument()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('uses the table label for table summaries', () => {
|
|
162
|
+
renderColumn({
|
|
163
|
+
data: { width: 12, widget: 'table-widget' },
|
|
164
|
+
visualizations: {
|
|
165
|
+
'table-widget': {
|
|
166
|
+
uid: 'table-widget',
|
|
167
|
+
type: 'table',
|
|
168
|
+
visualizationType: 'table',
|
|
169
|
+
table: { label: 'Custom Table' }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(screen.getByText('Custom Table')).toBeInTheDocument()
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import React, { useContext } from 'react'
|
|
2
2
|
import { useDrop } from 'react-dnd'
|
|
3
3
|
|
|
4
|
-
import { DashboardContext } from '../DashboardContext'
|
|
4
|
+
import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
|
|
5
|
+
import { DashboardCopyPasteContext } from '../DashboardCopyPasteContext'
|
|
5
6
|
import Widget from './Widget/Widget'
|
|
7
|
+
import { getColumnWidgetEntries, hasConditionalWidgets } from '../helpers/dashboardColumnWidgets'
|
|
6
8
|
|
|
7
9
|
type ColumnProps = {
|
|
8
10
|
// column data passed from parent
|
|
9
|
-
data:
|
|
11
|
+
data: any
|
|
10
12
|
// row index
|
|
11
13
|
rowIdx: number
|
|
12
14
|
// column index
|
|
@@ -15,8 +17,54 @@ type ColumnProps = {
|
|
|
15
17
|
toggleRow: boolean
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const
|
|
20
|
+
const formatSummaryList = items => {
|
|
21
|
+
const summaryItems = items.filter(Boolean)
|
|
22
|
+
if (!summaryItems.length) return undefined
|
|
23
|
+
|
|
24
|
+
return summaryItems.join(', ')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getDashboardFiltersTitle = (config, sharedFilters = []) => {
|
|
28
|
+
const filters = config.sharedFilterIndexes?.map(index => sharedFilters[Number(index)]).filter(Boolean) || []
|
|
29
|
+
return formatSummaryList(
|
|
30
|
+
filters.map(
|
|
31
|
+
filter =>
|
|
32
|
+
filter.key?.trim() ||
|
|
33
|
+
filter.columnName ||
|
|
34
|
+
filter.apiFilter?.textSelector ||
|
|
35
|
+
filter.apiFilter?.valueSelector ||
|
|
36
|
+
filter.queryParameter
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleTitle = (config, sharedFilters = []) => {
|
|
42
|
+
if (!config) return
|
|
43
|
+
if (config.type === 'map') return config.general.title
|
|
44
|
+
if (config.type === 'markup-include') return config.contentEditor?.title
|
|
45
|
+
if (config.type === 'dashboardFilters') return getDashboardFiltersTitle(config, sharedFilters)
|
|
46
|
+
if (config.type === 'table') return config.table?.label
|
|
47
|
+
return config.title
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type ConditionalColumnProps = {
|
|
51
|
+
data: any
|
|
52
|
+
rowIdx: number
|
|
53
|
+
colIdx: number
|
|
54
|
+
toggleRow: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type SimpleColumnProps = {
|
|
58
|
+
data: any
|
|
59
|
+
rowIdx: number
|
|
60
|
+
colIdx: number
|
|
61
|
+
toggleRow: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SimpleColumn: React.FC<SimpleColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
|
|
19
65
|
const { config } = useContext(DashboardContext)
|
|
66
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
67
|
+
const { copiedWidget, clearCopiedWidget } = useContext(DashboardCopyPasteContext)
|
|
20
68
|
|
|
21
69
|
const [{ isOver, canDrop }, drop] = useDrop(
|
|
22
70
|
() => ({
|
|
@@ -32,7 +80,7 @@ const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
|
|
|
32
80
|
canDrop: !!monitor.canDrop()
|
|
33
81
|
})
|
|
34
82
|
}),
|
|
35
|
-
[config.activeDashboard]
|
|
83
|
+
[config.activeDashboard, rowIdx, colIdx, data.widget]
|
|
36
84
|
)
|
|
37
85
|
|
|
38
86
|
const widget = data.widget ? config?.visualizations[data.widget] : null
|
|
@@ -46,21 +94,38 @@ const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
|
|
|
46
94
|
|
|
47
95
|
if (widget) {
|
|
48
96
|
classNames.push('column--populated')
|
|
97
|
+
} else if (copiedWidget) {
|
|
98
|
+
classNames.push('column--paste-ready')
|
|
49
99
|
}
|
|
50
100
|
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
101
|
+
const pasteCopiedWidget = () => {
|
|
102
|
+
if (!copiedWidget || widget) return
|
|
103
|
+
|
|
104
|
+
dispatch({
|
|
105
|
+
type: 'CLONE_VISUALIZATION',
|
|
106
|
+
payload: { sourceWidgetKey: copiedWidget.sourceWidgetKey, rowIdx, colIdx }
|
|
107
|
+
})
|
|
108
|
+
clearCopiedWidget()
|
|
56
109
|
}
|
|
57
110
|
|
|
58
111
|
return (
|
|
59
|
-
<div
|
|
112
|
+
<div
|
|
113
|
+
className={classNames.join(' ')}
|
|
114
|
+
ref={drop}
|
|
115
|
+
onClick={pasteCopiedWidget}
|
|
116
|
+
onKeyDown={event => {
|
|
117
|
+
if (!copiedWidget || widget) return
|
|
118
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
119
|
+
event.preventDefault()
|
|
120
|
+
pasteCopiedWidget()
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
role={!widget && copiedWidget ? 'button' : undefined}
|
|
124
|
+
tabIndex={!widget && copiedWidget ? 0 : undefined}
|
|
125
|
+
>
|
|
60
126
|
{widget ? (
|
|
61
127
|
<Widget
|
|
62
|
-
|
|
63
|
-
title={handleTitle(widget)}
|
|
128
|
+
title={handleTitle(widget, config.dashboard?.sharedFilters)}
|
|
64
129
|
widgetConfig={{ rowIdx, colIdx, ...widget }}
|
|
65
130
|
type={widget.visualizationType ?? widget.general?.geoType}
|
|
66
131
|
toggleRow={toggleRow}
|
|
@@ -68,11 +133,147 @@ const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
|
|
|
68
133
|
/>
|
|
69
134
|
) : (
|
|
70
135
|
<p className='builder-column__text'>
|
|
71
|
-
|
|
136
|
+
{copiedWidget ? (
|
|
137
|
+
'Click here to paste copied component or drag and drop a new visualization'
|
|
138
|
+
) : (
|
|
139
|
+
<>
|
|
140
|
+
Drag and drop <br /> visualization
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type ConditionalColumnSlotProps = {
|
|
150
|
+
rowIdx: number
|
|
151
|
+
colIdx: number
|
|
152
|
+
toggleRow: boolean
|
|
153
|
+
entryIndex?: number
|
|
154
|
+
widgetKey?: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ConditionalColumnSlot: React.FC<ConditionalColumnSlotProps> = ({
|
|
158
|
+
rowIdx,
|
|
159
|
+
colIdx,
|
|
160
|
+
toggleRow,
|
|
161
|
+
entryIndex,
|
|
162
|
+
widgetKey
|
|
163
|
+
}) => {
|
|
164
|
+
const { config } = useContext(DashboardContext)
|
|
165
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
166
|
+
const { copiedWidget, clearCopiedWidget } = useContext(DashboardCopyPasteContext)
|
|
167
|
+
|
|
168
|
+
const [{ isOver, canDrop }, drop] = useDrop(
|
|
169
|
+
() => ({
|
|
170
|
+
accept: 'vis-widget',
|
|
171
|
+
drop: () => ({
|
|
172
|
+
rowIdx,
|
|
173
|
+
colIdx,
|
|
174
|
+
entryIdx: entryIndex,
|
|
175
|
+
canDrop
|
|
176
|
+
}),
|
|
177
|
+
canDrop: () => !widgetKey,
|
|
178
|
+
collect: monitor => ({
|
|
179
|
+
isOver: monitor.isOver(),
|
|
180
|
+
canDrop: !!monitor.canDrop()
|
|
181
|
+
})
|
|
182
|
+
}),
|
|
183
|
+
[config.activeDashboard, rowIdx, colIdx, entryIndex, widgetKey]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const widget = widgetKey ? config?.visualizations[widgetKey] : null
|
|
187
|
+
if (widget && !widget.uid) widget.uid = widgetKey
|
|
188
|
+
const classNames = ['builder-column--conditional__slot']
|
|
189
|
+
|
|
190
|
+
if (isOver && canDrop) {
|
|
191
|
+
classNames.push('column--drop')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (widget) {
|
|
195
|
+
classNames.push('column--populated')
|
|
196
|
+
} else if (copiedWidget) {
|
|
197
|
+
classNames.push('column--paste-ready')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pasteCopiedWidget = () => {
|
|
201
|
+
if (!copiedWidget || widget) return
|
|
202
|
+
|
|
203
|
+
dispatch({
|
|
204
|
+
type: 'CLONE_VISUALIZATION',
|
|
205
|
+
payload: { sourceWidgetKey: copiedWidget.sourceWidgetKey, rowIdx, colIdx, entryIdx: entryIndex }
|
|
206
|
+
})
|
|
207
|
+
clearCopiedWidget()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div
|
|
212
|
+
className={classNames.join(' ')}
|
|
213
|
+
ref={drop}
|
|
214
|
+
onClick={pasteCopiedWidget}
|
|
215
|
+
onKeyDown={event => {
|
|
216
|
+
if (!copiedWidget || widget) return
|
|
217
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
218
|
+
event.preventDefault()
|
|
219
|
+
pasteCopiedWidget()
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
222
|
+
role={!widget && copiedWidget ? 'button' : undefined}
|
|
223
|
+
tabIndex={!widget && copiedWidget ? 0 : undefined}
|
|
224
|
+
>
|
|
225
|
+
{widget ? (
|
|
226
|
+
<Widget
|
|
227
|
+
title={handleTitle(widget, config.dashboard?.sharedFilters)}
|
|
228
|
+
widgetConfig={{ rowIdx, colIdx, entryIdx: entryIndex, ...widget }}
|
|
229
|
+
type={widget.visualizationType ?? widget.general?.geoType}
|
|
230
|
+
toggleRow={toggleRow}
|
|
231
|
+
widgetInRow
|
|
232
|
+
/>
|
|
233
|
+
) : (
|
|
234
|
+
<p className='builder-column__text'>
|
|
235
|
+
{copiedWidget ? (
|
|
236
|
+
'Click here to paste copied alternate visualization or drag and drop a new alternate'
|
|
237
|
+
) : (
|
|
238
|
+
<>
|
|
239
|
+
Drag and drop an alternate visualization.
|
|
240
|
+
<span className='builder-column__hint'>
|
|
241
|
+
If multiple conditions match, only the first match in this column is shown.
|
|
242
|
+
</span>
|
|
243
|
+
</>
|
|
244
|
+
)}
|
|
72
245
|
</p>
|
|
73
246
|
)}
|
|
74
247
|
</div>
|
|
75
248
|
)
|
|
76
249
|
}
|
|
77
250
|
|
|
251
|
+
const ConditionalColumn: React.FC<ConditionalColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
|
|
252
|
+
const widgetEntries = getColumnWidgetEntries(data)
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div className={['builder-column', 'builder-column--conditional', 'column-size--' + data.width].join(' ')}>
|
|
256
|
+
{widgetEntries.map((entry, entryIndex) => (
|
|
257
|
+
<ConditionalColumnSlot
|
|
258
|
+
key={`${rowIdx}-${colIdx}-${entryIndex}-${entry.widget}`}
|
|
259
|
+
rowIdx={rowIdx}
|
|
260
|
+
colIdx={colIdx}
|
|
261
|
+
toggleRow={toggleRow}
|
|
262
|
+
entryIndex={entryIndex}
|
|
263
|
+
widgetKey={entry.widget}
|
|
264
|
+
/>
|
|
265
|
+
))}
|
|
266
|
+
<ConditionalColumnSlot rowIdx={rowIdx} colIdx={colIdx} toggleRow={toggleRow} entryIndex={widgetEntries.length} />
|
|
267
|
+
</div>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const Column: React.FC<ColumnProps> = ({ data, rowIdx, colIdx, toggleRow }) => {
|
|
272
|
+
if (hasConditionalWidgets(data)) {
|
|
273
|
+
return <ConditionalColumn data={data} rowIdx={rowIdx} colIdx={colIdx} toggleRow={toggleRow} />
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return <SimpleColumn data={data} rowIdx={rowIdx} colIdx={colIdx} toggleRow={toggleRow} />
|
|
277
|
+
}
|
|
278
|
+
|
|
78
279
|
export default Column
|