@cdc/editor 4.26.2 → 4.26.4
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/dist/cdceditor-CY9IcPSi.es.js +6 -0
- package/dist/cdceditor-DlpiY3fQ.es.js +4 -0
- package/dist/cdceditor.js +84436 -77912
- package/package.json +9 -9
- package/src/CdcEditor.tsx +2 -3
- package/src/_stories/Editor.stories.tsx +173 -6
- package/src/components/ChooseTab.test.tsx +36 -0
- package/src/components/ChooseTab.tsx +39 -26
- package/src/components/DataImport/components/DataImport.tsx +124 -35
- package/src/components/DataImport/helpers/applyAutoDetectedDateParseFormat.ts +35 -0
- package/src/components/DataImport/tests/applyAutoDetectedDateParseFormat.test.ts +128 -0
- package/src/components/PreviewDataTable.test.tsx +184 -0
- package/src/components/PreviewDataTable.tsx +55 -49
- package/src/components/modal/Confirmation.jsx +5 -4
- package/src/scss/main.scss +15 -2
- package/dist/cdceditor-Cf9_fbQf.es.js +0 -6
- package/example/data-horizontal-filters.json +0 -8
- package/example/data-horizontal-multiseries-filters.json +0 -18
- package/example/data-horizontal-multiseries.json +0 -6
- package/example/data-horizontal.json +0 -4
- package/example/data-vertical-filters.json +0 -10
- package/example/data-vertical-multiseries-filters.json +0 -18
- package/example/data-vertical-multiseries-multirow-filters.json +0 -50
- package/example/data-vertical-multiseries-multirow.json +0 -14
- package/example/data-vertical-multiseries.json +0 -6
- package/example/data-vertical.json +0 -6
- package/example/region-map.json +0 -33
- package/example/test.json +0 -110280
- package/example/valid-county-data.json +0 -3049
- package/example/valid-scatterplot.csv +0 -17
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import ConfigContext, { EditorDispatchContext } from '@cdc/core/contexts/EditorContext'
|
|
4
|
+
import PreviewDataTable from './PreviewDataTable'
|
|
5
|
+
|
|
6
|
+
describe('PreviewDataTable', () => {
|
|
7
|
+
const renderPreview = config => {
|
|
8
|
+
const dispatch = vi.fn()
|
|
9
|
+
|
|
10
|
+
return render(
|
|
11
|
+
<ConfigContext.Provider
|
|
12
|
+
value={
|
|
13
|
+
{
|
|
14
|
+
config,
|
|
15
|
+
errors: [],
|
|
16
|
+
currentViewport: 'lg',
|
|
17
|
+
globalActive: 1,
|
|
18
|
+
setTempConfig: vi.fn()
|
|
19
|
+
} as any
|
|
20
|
+
}
|
|
21
|
+
>
|
|
22
|
+
<EditorDispatchContext.Provider value={dispatch}>
|
|
23
|
+
<PreviewDataTable />
|
|
24
|
+
</EditorDispatchContext.Provider>
|
|
25
|
+
</ConfigContext.Provider>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it('updates the rendered preview rows when the dashboard preview source changes', async () => {
|
|
30
|
+
const initialConfig = {
|
|
31
|
+
type: 'dashboard',
|
|
32
|
+
datasets: {
|
|
33
|
+
source_a: {
|
|
34
|
+
data: [{ label: 'Old Source Row', value: 'A' }],
|
|
35
|
+
preview: true
|
|
36
|
+
},
|
|
37
|
+
source_b: {
|
|
38
|
+
data: [{ label: 'Secondary Row', value: 'B' }],
|
|
39
|
+
preview: false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const nextConfig = {
|
|
45
|
+
...initialConfig,
|
|
46
|
+
datasets: {
|
|
47
|
+
source_b: {
|
|
48
|
+
data: [{ label: 'New Source Row', value: 'B2' }],
|
|
49
|
+
preview: true
|
|
50
|
+
},
|
|
51
|
+
source_a: {
|
|
52
|
+
data: [{ label: 'Old Source Row', value: 'A' }],
|
|
53
|
+
preview: false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const view = renderPreview(initialConfig)
|
|
59
|
+
|
|
60
|
+
expect(await screen.findByText('Old Source Row')).toBeInTheDocument()
|
|
61
|
+
expect(screen.queryByText('New Source Row')).not.toBeInTheDocument()
|
|
62
|
+
|
|
63
|
+
view.rerender(
|
|
64
|
+
<ConfigContext.Provider
|
|
65
|
+
value={
|
|
66
|
+
{
|
|
67
|
+
config: nextConfig,
|
|
68
|
+
errors: [],
|
|
69
|
+
currentViewport: 'lg',
|
|
70
|
+
globalActive: 1,
|
|
71
|
+
setTempConfig: vi.fn()
|
|
72
|
+
} as any
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
<EditorDispatchContext.Provider value={vi.fn()}>
|
|
76
|
+
<PreviewDataTable />
|
|
77
|
+
</EditorDispatchContext.Provider>
|
|
78
|
+
</ConfigContext.Provider>
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(screen.getByText('New Source Row')).toBeInTheDocument()
|
|
83
|
+
})
|
|
84
|
+
expect(screen.queryByText('Old Source Row')).not.toBeInTheDocument()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('shows another loaded dataset after the active preview dataset is removed', async () => {
|
|
88
|
+
const initialConfig = {
|
|
89
|
+
type: 'dashboard',
|
|
90
|
+
datasets: {
|
|
91
|
+
source_a: {
|
|
92
|
+
data: [{ label: 'Removed Source Row', value: 'A' }],
|
|
93
|
+
preview: true
|
|
94
|
+
},
|
|
95
|
+
source_b: {
|
|
96
|
+
data: [{ label: 'Fallback Source Row', value: 'B' }],
|
|
97
|
+
preview: false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nextConfig = {
|
|
103
|
+
type: 'dashboard',
|
|
104
|
+
datasets: {
|
|
105
|
+
source_b: {
|
|
106
|
+
data: [{ label: 'Fallback Source Row', value: 'B' }],
|
|
107
|
+
preview: true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const view = renderPreview(initialConfig)
|
|
113
|
+
|
|
114
|
+
expect(await screen.findByText('Removed Source Row')).toBeInTheDocument()
|
|
115
|
+
|
|
116
|
+
view.rerender(
|
|
117
|
+
<ConfigContext.Provider
|
|
118
|
+
value={
|
|
119
|
+
{
|
|
120
|
+
config: nextConfig,
|
|
121
|
+
errors: [],
|
|
122
|
+
currentViewport: 'lg',
|
|
123
|
+
globalActive: 1,
|
|
124
|
+
setTempConfig: vi.fn()
|
|
125
|
+
} as any
|
|
126
|
+
}
|
|
127
|
+
>
|
|
128
|
+
<EditorDispatchContext.Provider value={vi.fn()}>
|
|
129
|
+
<PreviewDataTable />
|
|
130
|
+
</EditorDispatchContext.Provider>
|
|
131
|
+
</ConfigContext.Provider>
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(screen.getByText('Fallback Source Row')).toBeInTheDocument()
|
|
136
|
+
})
|
|
137
|
+
expect(screen.queryByText('Removed Source Row')).not.toBeInTheDocument()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('returns to the initial empty preview when the final dataset is removed', async () => {
|
|
141
|
+
const initialConfig = {
|
|
142
|
+
type: 'dashboard',
|
|
143
|
+
datasets: {
|
|
144
|
+
source_a: {
|
|
145
|
+
data: [{ label: 'Last Source Row', value: 'A' }],
|
|
146
|
+
preview: true
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const nextConfig = {
|
|
152
|
+
type: 'dashboard',
|
|
153
|
+
datasets: {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const view = renderPreview(initialConfig)
|
|
157
|
+
|
|
158
|
+
expect(await screen.findByText('Last Source Row')).toBeInTheDocument()
|
|
159
|
+
|
|
160
|
+
view.rerender(
|
|
161
|
+
<ConfigContext.Provider
|
|
162
|
+
value={
|
|
163
|
+
{
|
|
164
|
+
config: nextConfig,
|
|
165
|
+
errors: [],
|
|
166
|
+
currentViewport: 'lg',
|
|
167
|
+
globalActive: 1,
|
|
168
|
+
setTempConfig: vi.fn()
|
|
169
|
+
} as any
|
|
170
|
+
}
|
|
171
|
+
>
|
|
172
|
+
<EditorDispatchContext.Provider value={vi.fn()}>
|
|
173
|
+
<PreviewDataTable />
|
|
174
|
+
</EditorDispatchContext.Provider>
|
|
175
|
+
</ConfigContext.Provider>
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(screen.getByText('No Data')).toBeInTheDocument()
|
|
180
|
+
})
|
|
181
|
+
expect(screen.getByText('Import data to preview')).toBeInTheDocument()
|
|
182
|
+
expect(screen.queryByText('Last Source Row')).not.toBeInTheDocument()
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useContext, useMemo,
|
|
1
|
+
import React, { useState, useContext, useMemo, useEffect, useRef, memo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
useTable,
|
|
4
4
|
useBlockLayout,
|
|
@@ -18,18 +18,21 @@ import { GrFormPrevious } from 'react-icons/gr'
|
|
|
18
18
|
import validateFipsCodeLength from '@cdc/core/helpers/validateFipsCodeLength'
|
|
19
19
|
import { errorMessages } from '../helpers/errorMessages'
|
|
20
20
|
import { DataSet } from '@cdc/core/types/DataSet'
|
|
21
|
-
import
|
|
21
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
22
22
|
|
|
23
|
-
const TableFilter = memo(({ globalFilter, setGlobalFilter, disabled = false }: any) => {
|
|
24
|
-
const [filterValue, setFilterValue] = useState(globalFilter)
|
|
23
|
+
const TableFilter = memo(({ globalFilter, setGlobalFilter = () => {}, disabled = false }: any) => {
|
|
24
|
+
const [filterValue, setFilterValue] = useState(globalFilter ?? '')
|
|
25
25
|
|
|
26
26
|
const [debouncedValue] = useDebounce(filterValue, 200)
|
|
27
27
|
|
|
28
28
|
useEffect(() => {
|
|
29
|
-
if ('string' === typeof debouncedValue &&
|
|
30
|
-
|
|
29
|
+
if ('string' === typeof debouncedValue && typeof setGlobalFilter === 'function') {
|
|
30
|
+
const nextFilter = debouncedValue.trim() ? debouncedValue : undefined
|
|
31
|
+
if (nextFilter !== globalFilter) {
|
|
32
|
+
setGlobalFilter(nextFilter)
|
|
33
|
+
}
|
|
31
34
|
}
|
|
32
|
-
}, [debouncedValue])
|
|
35
|
+
}, [debouncedValue, globalFilter, setGlobalFilter])
|
|
33
36
|
|
|
34
37
|
const onChange = e => {
|
|
35
38
|
setFilterValue(e.target.value)
|
|
@@ -58,25 +61,27 @@ const Footer = memo(({ previousPage, nextPage, canPreviousPage, canNextPage, pag
|
|
|
58
61
|
<footer className='data-table-pagination mt-2'>
|
|
59
62
|
<ul>
|
|
60
63
|
<li>
|
|
61
|
-
<
|
|
64
|
+
<Button
|
|
62
65
|
onClick={() => previousPage()}
|
|
63
66
|
className='btn btn-prev display-flex align-items-center justify-content-center'
|
|
64
67
|
disabled={!canPreviousPage}
|
|
65
68
|
title='Previous Page'
|
|
69
|
+
flexCenter
|
|
66
70
|
>
|
|
67
71
|
{' '}
|
|
68
72
|
<GrFormPrevious />
|
|
69
|
-
</
|
|
73
|
+
</Button>
|
|
70
74
|
</li>
|
|
71
75
|
<li className='me-2'>
|
|
72
|
-
<
|
|
76
|
+
<Button
|
|
73
77
|
onClick={() => nextPage()}
|
|
74
78
|
className='btn btn-next display-flex align-items-center justify-content-center'
|
|
75
79
|
disabled={!canNextPage}
|
|
76
80
|
title='Next Page'
|
|
81
|
+
flexCenter
|
|
77
82
|
>
|
|
78
83
|
<MdNavigateNext />
|
|
79
|
-
</
|
|
84
|
+
</Button>
|
|
80
85
|
</li>
|
|
81
86
|
</ul>
|
|
82
87
|
<span>
|
|
@@ -86,30 +91,45 @@ const Footer = memo(({ previousPage, nextPage, canPreviousPage, canNextPage, pag
|
|
|
86
91
|
))
|
|
87
92
|
|
|
88
93
|
const PreviewDataTable = () => {
|
|
89
|
-
const { config } = useContext(ConfigContext)
|
|
94
|
+
const { config, errors } = useContext(ConfigContext)
|
|
90
95
|
const previewData = useMemo(() => {
|
|
91
96
|
if (config.type === 'dashboard') {
|
|
92
|
-
|
|
97
|
+
const previewDataset = Object.values(config.datasets).find((dataset: DataSet) => {
|
|
93
98
|
return dataset.preview && Array.isArray(dataset.data)
|
|
94
99
|
})
|
|
100
|
+
return previewDataset?.data
|
|
95
101
|
}
|
|
96
102
|
return config.data
|
|
97
103
|
}, [config.type, config.data, config.datasets])
|
|
98
|
-
const [tableData, _setTableData] = useState(previewData)
|
|
104
|
+
const [tableData, _setTableData] = useState(Array.isArray(previewData) ? previewData : null)
|
|
105
|
+
const lastDataSourceRef = useRef<any[]>(null)
|
|
99
106
|
const runSideEffects = (td: any[]) => {
|
|
107
|
+
if (!Array.isArray(td) || td.length === 0) return td
|
|
108
|
+
|
|
100
109
|
const isSankey = Object.keys(td[0]).includes('tableData')
|
|
101
|
-
const
|
|
102
|
-
validateFipsCodeLength(
|
|
103
|
-
return
|
|
110
|
+
const normalizedData = isSankey ? td[0].tableData : td
|
|
111
|
+
validateFipsCodeLength(normalizedData)
|
|
112
|
+
return normalizedData
|
|
113
|
+
}
|
|
114
|
+
const setTableData = td => {
|
|
115
|
+
if (!Array.isArray(td) || td.length === 0) {
|
|
116
|
+
// When the active dataset is removed, clear the cached source so the placeholder can render again.
|
|
117
|
+
lastDataSourceRef.current = null
|
|
118
|
+
_setTableData(null)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if (lastDataSourceRef.current === td) return
|
|
122
|
+
|
|
123
|
+
lastDataSourceRef.current = td
|
|
124
|
+
_setTableData(runSideEffects(td))
|
|
104
125
|
}
|
|
105
|
-
const setTableData = td => _setTableData(runSideEffects(td))
|
|
106
126
|
|
|
107
127
|
const dispatch = useContext(EditorDispatchContext)
|
|
108
128
|
|
|
109
129
|
const fetchDatasetData = async (datasetKey, datasetConfig) => {
|
|
110
130
|
if (datasetConfig.preview) {
|
|
111
131
|
if (datasetConfig.dataUrl) {
|
|
112
|
-
const remoteData = await fetchRemoteData(datasetConfig.dataUrl)
|
|
132
|
+
const { data: remoteData } = await fetchRemoteData(datasetConfig.dataUrl)
|
|
113
133
|
if (Array.isArray(remoteData)) {
|
|
114
134
|
setTableData(remoteData)
|
|
115
135
|
}
|
|
@@ -129,10 +149,14 @@ const PreviewDataTable = () => {
|
|
|
129
149
|
const loadData = async () => {
|
|
130
150
|
if (!config.data) {
|
|
131
151
|
if (config.type === 'dashboard') {
|
|
152
|
+
if (!previewData) {
|
|
153
|
+
setTableData(null)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
132
156
|
await handleDashboardData(config.datasets)
|
|
133
157
|
} else {
|
|
134
158
|
if (config.dataUrl) {
|
|
135
|
-
const remoteData = await fetchRemoteData(config.dataUrl)
|
|
159
|
+
const { data: remoteData } = await fetchRemoteData(config.dataUrl)
|
|
136
160
|
if (Array.isArray(remoteData)) {
|
|
137
161
|
setTableData(remoteData)
|
|
138
162
|
}
|
|
@@ -144,16 +168,11 @@ const PreviewDataTable = () => {
|
|
|
144
168
|
}
|
|
145
169
|
|
|
146
170
|
loadData()
|
|
147
|
-
}, [config, config.
|
|
171
|
+
}, [config.data, config.dataUrl, config.datasets, config.type, previewData]) // eslint-disable-line
|
|
148
172
|
|
|
149
173
|
const tableColumns = useMemo(() => {
|
|
150
|
-
if (!tableData) return []
|
|
151
|
-
const columns = tableData
|
|
152
|
-
if (columns.length > 0 && columns.includes('')) {
|
|
153
|
-
// todo find a way to call the errors. Currently they are in DataImport.js
|
|
154
|
-
// maybe these can be moved to a file? but then we need a way to add settings like size...
|
|
155
|
-
dispatch({ type: 'EDITOR_SET_ERRORS', payload: [errorMessages.emptyCols] })
|
|
156
|
-
}
|
|
174
|
+
if (!Array.isArray(tableData)) return []
|
|
175
|
+
const columns = Object.keys(tableData[0] ?? {})
|
|
157
176
|
|
|
158
177
|
return columns.map(columnName => {
|
|
159
178
|
const columnConfig = {
|
|
@@ -166,27 +185,13 @@ const PreviewDataTable = () => {
|
|
|
166
185
|
})
|
|
167
186
|
}, [tableData])
|
|
168
187
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
Object.keys(rowObj).forEach(columnHeading => {
|
|
175
|
-
if (false === columns.includes(columnHeading)) {
|
|
176
|
-
columns.push(columnHeading)
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
// D3 uses a weird quirk where it attaches a named property to an array. Replicating here.
|
|
182
|
-
type D3Data = any[] & { columns }
|
|
183
|
-
const newData: D3Data = [...data] as D3Data
|
|
184
|
-
|
|
185
|
-
if (Array.isArray(newData)) {
|
|
186
|
-
newData.columns = columns
|
|
187
|
-
return newData
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!tableData) return
|
|
190
|
+
const columns = Object.keys(tableData[0] ?? {})
|
|
191
|
+
if (columns.length > 0 && columns.includes('') && !errors?.includes(errorMessages.emptyCols)) {
|
|
192
|
+
dispatch({ type: 'EDITOR_SET_ERRORS', payload: [errorMessages.emptyCols] })
|
|
188
193
|
}
|
|
189
|
-
}
|
|
194
|
+
}, [dispatch, errors, tableData])
|
|
190
195
|
|
|
191
196
|
const {
|
|
192
197
|
getTableProps,
|
|
@@ -245,7 +250,8 @@ const PreviewDataTable = () => {
|
|
|
245
250
|
)
|
|
246
251
|
}
|
|
247
252
|
|
|
248
|
-
if (!tableData)
|
|
253
|
+
if (!Array.isArray(tableData) || tableData.length === 0)
|
|
254
|
+
return [<Header key='header' />, <PlaceholderTable key='table' />]
|
|
249
255
|
|
|
250
256
|
const footerProps = {
|
|
251
257
|
previousPage,
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
2
3
|
|
|
3
4
|
export const ConfirmationModal = props => {
|
|
4
5
|
return (
|
|
5
6
|
<>
|
|
6
7
|
<p className='message'>{props.message}</p>
|
|
7
8
|
<div className='confirmation-buttons'>
|
|
8
|
-
<
|
|
9
|
+
<Button type='button' className='btn btn-inline' onClick={props.onCancel}>
|
|
9
10
|
No
|
|
10
|
-
</
|
|
11
|
-
<
|
|
11
|
+
</Button>
|
|
12
|
+
<Button type='button' className='btn btn-inline' onClick={props.onConfirm}>
|
|
12
13
|
Yes
|
|
13
|
-
</
|
|
14
|
+
</Button>
|
|
14
15
|
</div>
|
|
15
16
|
</>
|
|
16
17
|
)
|
package/src/scss/main.scss
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
@import 'variables';
|
|
2
2
|
|
|
3
|
-
.
|
|
3
|
+
.cove-visualization.cdc-editor {
|
|
4
4
|
min-height: inherit;
|
|
5
5
|
display: flex;
|
|
6
6
|
flex-direction: column;
|
|
@@ -202,9 +202,22 @@
|
|
|
202
202
|
font-size: 1em;
|
|
203
203
|
display: block;
|
|
204
204
|
border-radius: 5px;
|
|
205
|
-
transition:
|
|
205
|
+
transition: background-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
|
206
206
|
cursor: pointer;
|
|
207
207
|
|
|
208
|
+
@media (hover: hover) {
|
|
209
|
+
&:hover:not(:disabled) {
|
|
210
|
+
background: #0b4778;
|
|
211
|
+
transform: translateY(-1px);
|
|
212
|
+
box-shadow: 0 0.5rem 1.1rem rgb(0 94 170 / 22%);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
&:active:not(:disabled) {
|
|
217
|
+
transform: translateY(0);
|
|
218
|
+
box-shadow: 0 0.15rem 0.45rem rgb(0 94 170 / 12%);
|
|
219
|
+
}
|
|
220
|
+
|
|
208
221
|
&.btn-inline {
|
|
209
222
|
display: inline-block;
|
|
210
223
|
margin: 10px 0 0 10px;
|