@cdc/editor 4.26.2 → 4.26.3
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/LICENSE +201 -0
- package/dist/cdceditor-vr9HZwRt.es.js +6 -0
- package/dist/cdceditor.js +57548 -53851
- package/package.json +9 -9
- package/src/CdcEditor.tsx +2 -3
- package/src/_stories/Editor.stories.tsx +111 -6
- package/src/components/ChooseTab.test.tsx +36 -0
- package/src/components/ChooseTab.tsx +34 -23
- package/src/components/DataImport/components/DataImport.tsx +10 -5
- package/src/components/PreviewDataTable.tsx +39 -44
- package/src/scss/main.scss +1 -1
- package/dist/cdceditor-Cf9_fbQf.es.js +0 -6
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/editor",
|
|
3
|
-
"version": "4.26.
|
|
3
|
+
"version": "4.26.3",
|
|
4
4
|
"description": "React component for generating a new component entry",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bugs": "https://github.com/CDCgov/cdc-open-viz/issues",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@cdc/chart": "^4.26.
|
|
9
|
-
"@cdc/core": "^4.26.
|
|
10
|
-
"@cdc/dashboard": "^4.26.
|
|
11
|
-
"@cdc/data-bite": "^4.26.
|
|
12
|
-
"@cdc/map": "^4.26.
|
|
13
|
-
"@cdc/markup-include": "^4.26.
|
|
14
|
-
"@cdc/waffle-chart": "^4.26.
|
|
8
|
+
"@cdc/chart": "^4.26.3",
|
|
9
|
+
"@cdc/core": "^4.26.3",
|
|
10
|
+
"@cdc/dashboard": "^4.26.3",
|
|
11
|
+
"@cdc/data-bite": "^4.26.3",
|
|
12
|
+
"@cdc/map": "^4.26.3",
|
|
13
|
+
"@cdc/markup-include": "^4.26.3",
|
|
14
|
+
"@cdc/waffle-chart": "^4.26.3",
|
|
15
15
|
"axios": "^1.13.2",
|
|
16
16
|
"d3": "^7.9.0",
|
|
17
17
|
"react-dropzone": "^14.3.8",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"vite-plugin-css-injected-by-js": "^2.4.0",
|
|
24
24
|
"vite-plugin-svgr": "^4.2.0"
|
|
25
25
|
},
|
|
26
|
-
"gitHead": "
|
|
26
|
+
"gitHead": "d50e45a074fbefa56cac904917e707d57f237737",
|
|
27
27
|
"main": "dist/cdceditor",
|
|
28
28
|
"moduleName": "CdcEditor",
|
|
29
29
|
"peerDependencies": {
|
package/src/CdcEditor.tsx
CHANGED
|
@@ -19,7 +19,6 @@ import { legacyConfigSupport } from './helpers/legacyConfigSupport'
|
|
|
19
19
|
|
|
20
20
|
import './scss/main.scss'
|
|
21
21
|
import editorReducer, { EditorState } from '@cdc/core/contexts/editor.reducer'
|
|
22
|
-
import _ from 'lodash'
|
|
23
22
|
import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
|
|
24
23
|
import { WCMSProps } from '@cdc/core/types/WCMSProps'
|
|
25
24
|
import { devToolsStore } from '@cdc/core/helpers/withDevTools'
|
|
@@ -73,7 +72,7 @@ const CdcEditor: React.FC<WCMSProps> = ({ config: configObj, hostname, container
|
|
|
73
72
|
}, [])
|
|
74
73
|
|
|
75
74
|
useEffect(() => {
|
|
76
|
-
let strippedConfig = stripConfig(state.config)
|
|
75
|
+
let strippedConfig = stripConfig(state.config, true)
|
|
77
76
|
|
|
78
77
|
const parsedData = JSON.stringify(strippedConfig)
|
|
79
78
|
// Emit the data in a regular JS event so it can be consumed by anything.
|
|
@@ -99,7 +98,7 @@ const CdcEditor: React.FC<WCMSProps> = ({ config: configObj, hostname, container
|
|
|
99
98
|
<GlobalContextProvider>
|
|
100
99
|
<ConfigContext.Provider value={{ ...state, setTempConfig: setTempConfigAndUpdate }}>
|
|
101
100
|
<EditorDispatchContext.Provider value={dispatch}>
|
|
102
|
-
<div className={`
|
|
101
|
+
<div className={`cove-visualization cdc-editor ${state.currentViewport}`} ref={outerContainerRef}>
|
|
103
102
|
<Tabs className='top-level'>
|
|
104
103
|
<TabPane title='1. Choose Visualization Type' className='choose-type'>
|
|
105
104
|
<ChooseTab />
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import CdcEditor from '../CdcEditor'
|
|
3
|
-
import { within, userEvent } from 'storybook/test'
|
|
3
|
+
import { within, userEvent, expect } from 'storybook/test'
|
|
4
|
+
import ChartEditorConfig from '../../../chart/src/_stories/_mock/editor-tests/bar-chart-editor-test.json'
|
|
5
|
+
import MapConfig from '../../../map/src/_stories/_mock/default-patterns.json'
|
|
6
|
+
import DashboardConfig from '../../../dashboard/src/_stories/_mock/dashboard_no_filter.json'
|
|
7
|
+
import DataTableConfig from '../../../data-table/examples/data-table-example.json'
|
|
4
8
|
|
|
5
|
-
const
|
|
6
|
-
|
|
9
|
+
const loadConfigFromTextArea = async (canvasElement, config) => {
|
|
10
|
+
const user = userEvent.setup()
|
|
11
|
+
const textArea = canvasElement.querySelector('#pasteConfig') as HTMLTextAreaElement
|
|
12
|
+
const loadButton = canvasElement.querySelector('#load-data') as HTMLButtonElement
|
|
13
|
+
|
|
14
|
+
expect(textArea).toBeTruthy()
|
|
15
|
+
expect(loadButton).toBeTruthy()
|
|
16
|
+
|
|
17
|
+
await user.click(textArea)
|
|
18
|
+
await user.clear(textArea)
|
|
19
|
+
await user.paste(JSON.stringify(config))
|
|
20
|
+
await user.click(loadButton)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const assertImportDataTabAccessible = async canvas => {
|
|
24
|
+
const user = userEvent.setup()
|
|
25
|
+
await user.click(canvas.getByText('2. Import Data'))
|
|
26
|
+
await expect(canvas.findByText('Data Preview')).resolves.toBeTruthy()
|
|
7
27
|
}
|
|
8
28
|
|
|
9
29
|
const meta: Meta<typeof CdcEditor> = {
|
|
@@ -26,10 +46,95 @@ export const PreviewTableTests: Story = {
|
|
|
26
46
|
play: async ({ canvasElement }) => {
|
|
27
47
|
const canvas = within(canvasElement)
|
|
28
48
|
const user = userEvent.setup()
|
|
29
|
-
|
|
30
|
-
const mapButton = canvas.
|
|
49
|
+
|
|
50
|
+
const mapButton = await canvas.findByRole('button', { name: 'United States (State- or County-Level)' })
|
|
31
51
|
await user.click(mapButton)
|
|
32
|
-
|
|
52
|
+
|
|
53
|
+
await user.click(canvas.getByText('2. Import Data'))
|
|
54
|
+
|
|
55
|
+
const sampleData = await canvas.findByText('United States: County Sample Data')
|
|
33
56
|
await user.click(sampleData)
|
|
57
|
+
|
|
58
|
+
await expect(canvas.findByText('Data Preview')).resolves.toBeTruthy()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const LoadChartJsonConfig: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
config: {}
|
|
65
|
+
},
|
|
66
|
+
play: async ({ canvasElement }) => {
|
|
67
|
+
const canvas = within(canvasElement)
|
|
68
|
+
await loadConfigFromTextArea(canvasElement, ChartEditorConfig)
|
|
69
|
+
await assertImportDataTabAccessible(canvas)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const LoadMapJsonConfig: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
config: {}
|
|
76
|
+
},
|
|
77
|
+
play: async ({ canvasElement }) => {
|
|
78
|
+
const canvas = within(canvasElement)
|
|
79
|
+
await loadConfigFromTextArea(canvasElement, MapConfig)
|
|
80
|
+
await assertImportDataTabAccessible(canvas)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const LoadDashboardJsonConfig: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
config: {}
|
|
87
|
+
},
|
|
88
|
+
play: async ({ canvasElement }) => {
|
|
89
|
+
const canvas = within(canvasElement)
|
|
90
|
+
await loadConfigFromTextArea(canvasElement, DashboardConfig)
|
|
91
|
+
await assertImportDataTabAccessible(canvas)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const LoadDataTableJsonConfig: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
config: {}
|
|
98
|
+
},
|
|
99
|
+
play: async ({ canvasElement }) => {
|
|
100
|
+
const canvas = within(canvasElement)
|
|
101
|
+
await loadConfigFromTextArea(canvasElement, DataTableConfig)
|
|
102
|
+
await assertImportDataTabAccessible(canvas)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const InvalidJsonShowsValidationAlert: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
config: {}
|
|
109
|
+
},
|
|
110
|
+
play: async ({ canvasElement }) => {
|
|
111
|
+
const user = userEvent.setup()
|
|
112
|
+
const textArea = canvasElement.querySelector('#pasteConfig') as HTMLTextAreaElement
|
|
113
|
+
const loadButton = canvasElement.querySelector('#load-data') as HTMLButtonElement
|
|
114
|
+
|
|
115
|
+
expect(textArea).toBeTruthy()
|
|
116
|
+
expect(loadButton).toBeTruthy()
|
|
117
|
+
|
|
118
|
+
const originalAlert = window.alert
|
|
119
|
+
const originalOnError = window.onerror
|
|
120
|
+
let alertText = ''
|
|
121
|
+
|
|
122
|
+
window.alert = message => {
|
|
123
|
+
alertText = String(message)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
window.onerror = () => true
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await user.click(textArea)
|
|
130
|
+
await user.clear(textArea)
|
|
131
|
+
await user.paste('{"broken": true, }')
|
|
132
|
+
await user.click(loadButton)
|
|
133
|
+
|
|
134
|
+
await expect(alertText).toBe('The JSON that was entered is invalid.')
|
|
135
|
+
} finally {
|
|
136
|
+
window.alert = originalAlert
|
|
137
|
+
window.onerror = originalOnError
|
|
138
|
+
}
|
|
34
139
|
}
|
|
35
140
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, waitFor } from '@testing-library/react'
|
|
3
|
+
|
|
4
|
+
import ConfigContext, { EditorDispatchContext } from '@cdc/core/contexts/EditorContext'
|
|
5
|
+
import ChooseTab from './ChooseTab'
|
|
6
|
+
|
|
7
|
+
describe('ChooseTab', () => {
|
|
8
|
+
it('dispatches EDITOR_SAVE once when tempConfig is present', async () => {
|
|
9
|
+
const dispatch = vi.fn()
|
|
10
|
+
const tempConfig = { type: 'chart', data: [{ x: 'A', y: 1 }] }
|
|
11
|
+
|
|
12
|
+
render(
|
|
13
|
+
<ConfigContext.Provider
|
|
14
|
+
value={
|
|
15
|
+
{
|
|
16
|
+
config: { type: 'chart' },
|
|
17
|
+
tempConfig,
|
|
18
|
+
errors: [],
|
|
19
|
+
currentViewport: 'lg',
|
|
20
|
+
globalActive: 0,
|
|
21
|
+
setTempConfig: vi.fn()
|
|
22
|
+
} as any
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
<EditorDispatchContext.Provider value={dispatch}>
|
|
26
|
+
<ChooseTab />
|
|
27
|
+
</EditorDispatchContext.Provider>
|
|
28
|
+
</ConfigContext.Provider>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
await waitFor(() => {
|
|
32
|
+
expect(dispatch).toHaveBeenCalledTimes(1)
|
|
33
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'EDITOR_SAVE', payload: tempConfig })
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useContext, useState } from 'react'
|
|
1
|
+
import React, { useContext, useEffect, useState } from 'react'
|
|
2
2
|
import '../scss/choose-vis-tab.scss'
|
|
3
3
|
|
|
4
4
|
import ConfigContext, { EditorDispatchContext } from '@cdc/core/contexts/EditorContext'
|
|
@@ -51,6 +51,23 @@ interface ButtonProps {
|
|
|
51
51
|
orientation?: string | null
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
interface VizButtonProps extends ButtonProps {
|
|
55
|
+
activeVizButtonID?: number
|
|
56
|
+
onConfigure: (props: Record<string, unknown>) => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const VizButton: React.FC<VizButtonProps> = ({ activeVizButtonID, onConfigure, ...buttonProps }) => {
|
|
60
|
+
const { label, icon, id } = buttonProps
|
|
61
|
+
const isActive = id === activeVizButtonID || 0
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<button className={isActive ? 'active' : ''} onClick={() => onConfigure(buttonProps)} aria-label={label}>
|
|
65
|
+
{icon}
|
|
66
|
+
<span className='mt-1'>{label}</span>
|
|
67
|
+
</button>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
const ChooseTab: React.FC = (): JSX.Element => {
|
|
55
72
|
const { config, tempConfig } = useContext(ConfigContext)
|
|
56
73
|
|
|
@@ -59,6 +76,12 @@ const ChooseTab: React.FC = (): JSX.Element => {
|
|
|
59
76
|
const dispatch = useContext(EditorDispatchContext)
|
|
60
77
|
const rowLabels = ['General', , 'Charts', 'Maps']
|
|
61
78
|
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (tempConfig) {
|
|
81
|
+
dispatch({ type: 'EDITOR_SAVE', payload: tempConfig })
|
|
82
|
+
}
|
|
83
|
+
}, [dispatch, tempConfig])
|
|
84
|
+
|
|
62
85
|
const handleUpload = e => {
|
|
63
86
|
const file = e.target.files[0]
|
|
64
87
|
const reader = new FileReader()
|
|
@@ -75,12 +98,15 @@ const ChooseTab: React.FC = (): JSX.Element => {
|
|
|
75
98
|
newConfig = JSON.parse(text)
|
|
76
99
|
} catch (e) {
|
|
77
100
|
alert('The JSON that was entered is invalid.')
|
|
78
|
-
|
|
101
|
+
return
|
|
79
102
|
}
|
|
80
103
|
|
|
81
104
|
const isVega = isVegaConfig(newConfig)
|
|
82
105
|
if (isVega) {
|
|
83
106
|
newConfig = importVegaConfig(newConfig)
|
|
107
|
+
if (!newConfig) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
dispatch({ type: 'EDITOR_SET_CONFIG', payload: newConfig })
|
|
@@ -112,7 +138,7 @@ const ChooseTab: React.FC = (): JSX.Element => {
|
|
|
112
138
|
|
|
113
139
|
const errorText = vegaErrors.join('\n\n')
|
|
114
140
|
alert(errorText)
|
|
115
|
-
|
|
141
|
+
return null
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
const generateNewConfig = props => {
|
|
@@ -174,25 +200,6 @@ const ChooseTab: React.FC = (): JSX.Element => {
|
|
|
174
200
|
dispatch({ type: 'EDITOR_SET_GLOBALACTIVE', payload: 1 })
|
|
175
201
|
}
|
|
176
202
|
|
|
177
|
-
const VizButton: React.FC<ButtonProps> = props => {
|
|
178
|
-
const { label, icon, id } = props
|
|
179
|
-
const isActive = id === config?.activeVizButtonID || 0
|
|
180
|
-
const handleClick = () => {
|
|
181
|
-
configureTabs(props)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (tempConfig) {
|
|
185
|
-
dispatch({ type: 'EDITOR_SAVE', payload: tempConfig })
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
<button className={isActive ? 'active' : ''} onClick={handleClick} aria-label={label}>
|
|
190
|
-
{icon}
|
|
191
|
-
<span className='mt-1'>{label}</span>
|
|
192
|
-
</button>
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
203
|
return (
|
|
197
204
|
<div className='choose-vis'>
|
|
198
205
|
<a
|
|
@@ -221,7 +228,11 @@ const ChooseTab: React.FC = (): JSX.Element => {
|
|
|
221
228
|
<li key={`${label}-button-${buttonIndex}`}>
|
|
222
229
|
<Tooltip position='right'>
|
|
223
230
|
<Tooltip.Target>
|
|
224
|
-
<VizButton
|
|
231
|
+
<VizButton
|
|
232
|
+
{...button}
|
|
233
|
+
activeVizButtonID={config?.activeVizButtonID}
|
|
234
|
+
onConfigure={configureTabs}
|
|
235
|
+
/>
|
|
225
236
|
</Tooltip.Target>
|
|
226
237
|
<Tooltip.Content>{button.content}</Tooltip.Content>
|
|
227
238
|
</Tooltip>
|
|
@@ -24,7 +24,7 @@ import { type Visualization } from '@cdc/core/types/Visualization'
|
|
|
24
24
|
import { type DataSet } from '@cdc/core/types/DataSet'
|
|
25
25
|
|
|
26
26
|
import './data-import.scss'
|
|
27
|
-
import '@cdc/core/
|
|
27
|
+
import '@cdc/core/components/managers/data-designer.scss'
|
|
28
28
|
|
|
29
29
|
import { errorMessages, maxFileSize } from '../../../helpers/errorMessages'
|
|
30
30
|
import { displaySize } from '../helpers/displaySize'
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
parseVegaConfig,
|
|
40
40
|
updateVegaData
|
|
41
41
|
} from '@cdc/core/helpers/vegaConfig'
|
|
42
|
+
import { extractDataAndMetadata } from '@cdc/core/helpers/extractDataAndMetadata'
|
|
42
43
|
|
|
43
44
|
const DataImport = () => {
|
|
44
45
|
const { config, errors, tempConfig, sharepath } = useContext(ConfigContext)
|
|
@@ -179,12 +180,13 @@ const DataImport = () => {
|
|
|
179
180
|
const filereader = new FileReader()
|
|
180
181
|
|
|
181
182
|
filereader.onload = function () {
|
|
182
|
-
const handleSetConfig = (newData: Object[], useTempConfig = false) => {
|
|
183
|
+
const handleSetConfig = (newData: Object[], useTempConfig = false, dataMetadata = {}) => {
|
|
183
184
|
const setDataURL = keepURL && fileSourceType === 'url'
|
|
184
185
|
if (config.type === 'dashboard') {
|
|
185
186
|
const dataFileFormat = mimeType.split('/')[1].toUpperCase()
|
|
186
187
|
const dataset = {
|
|
187
188
|
data: newData,
|
|
189
|
+
dataMetadata,
|
|
188
190
|
dataFileSize: fileSize,
|
|
189
191
|
dataFileName: fileSource, // new file source
|
|
190
192
|
dataFileSourceType: fileSourceType, // new file source type
|
|
@@ -210,6 +212,7 @@ const DataImport = () => {
|
|
|
210
212
|
...config,
|
|
211
213
|
...tempConfig,
|
|
212
214
|
data: newData,
|
|
215
|
+
dataMetadata,
|
|
213
216
|
dataFileName: fileSource, // new file source
|
|
214
217
|
dataFileSourceType: fileSourceType, // new file source type
|
|
215
218
|
formattedData: transform.developerStandardize(newData, config.dataDescription)
|
|
@@ -227,14 +230,16 @@ const DataImport = () => {
|
|
|
227
230
|
if (config.vegaConfig) {
|
|
228
231
|
return updateDataFromVegaData(result, fileSource, fileSourceType)
|
|
229
232
|
}
|
|
230
|
-
const
|
|
233
|
+
const { data: extractedData, dataMetadata } = extractDataAndMetadata(result)
|
|
234
|
+
const text = transform.autoStandardize(extractedData)
|
|
231
235
|
if (config.data && config.series) {
|
|
232
236
|
if (dataExists(text, config.series, config?.xAxis.dataKey)) {
|
|
233
|
-
handleSetConfig(text, true)
|
|
237
|
+
handleSetConfig(text, true, dataMetadata)
|
|
234
238
|
} else {
|
|
235
239
|
resetEditor(
|
|
236
240
|
{
|
|
237
241
|
data: text,
|
|
242
|
+
dataMetadata,
|
|
238
243
|
dataFileName: fileSource,
|
|
239
244
|
dataFileSourceType: fileSourceType
|
|
240
245
|
} as Visualization,
|
|
@@ -242,7 +247,7 @@ const DataImport = () => {
|
|
|
242
247
|
)
|
|
243
248
|
}
|
|
244
249
|
} else {
|
|
245
|
-
handleSetConfig(text)
|
|
250
|
+
handleSetConfig(text, false, dataMetadata)
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
if (editingDataset) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useContext, useMemo, useCallback, useEffect, memo } from 'react'
|
|
1
|
+
import React, { useState, useContext, useMemo, useCallback, useEffect, useRef, memo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
useTable,
|
|
4
4
|
useBlockLayout,
|
|
@@ -20,16 +20,19 @@ import { errorMessages } from '../helpers/errorMessages'
|
|
|
20
20
|
import { DataSet } from '@cdc/core/types/DataSet'
|
|
21
21
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
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)
|
|
@@ -86,30 +89,40 @@ const Footer = memo(({ previousPage, nextPage, canPreviousPage, canNextPage, pag
|
|
|
86
89
|
))
|
|
87
90
|
|
|
88
91
|
const PreviewDataTable = () => {
|
|
89
|
-
const { config } = useContext(ConfigContext)
|
|
92
|
+
const { config, errors } = useContext(ConfigContext)
|
|
90
93
|
const previewData = useMemo(() => {
|
|
91
94
|
if (config.type === 'dashboard') {
|
|
92
|
-
|
|
95
|
+
const previewDataset = Object.values(config.datasets).find((dataset: DataSet) => {
|
|
93
96
|
return dataset.preview && Array.isArray(dataset.data)
|
|
94
97
|
})
|
|
98
|
+
return previewDataset?.data
|
|
95
99
|
}
|
|
96
100
|
return config.data
|
|
97
101
|
}, [config.type, config.data, config.datasets])
|
|
98
|
-
const [tableData, _setTableData] = useState(previewData)
|
|
102
|
+
const [tableData, _setTableData] = useState(Array.isArray(previewData) ? previewData : null)
|
|
103
|
+
const lastDataSourceRef = useRef<any[]>(null)
|
|
99
104
|
const runSideEffects = (td: any[]) => {
|
|
105
|
+
if (!Array.isArray(td) || td.length === 0) return td
|
|
106
|
+
|
|
100
107
|
const isSankey = Object.keys(td[0]).includes('tableData')
|
|
101
|
-
const
|
|
102
|
-
validateFipsCodeLength(
|
|
103
|
-
return
|
|
108
|
+
const normalizedData = isSankey ? td[0].tableData : td
|
|
109
|
+
validateFipsCodeLength(normalizedData)
|
|
110
|
+
return normalizedData
|
|
111
|
+
}
|
|
112
|
+
const setTableData = td => {
|
|
113
|
+
if (!Array.isArray(td) || td.length === 0) return
|
|
114
|
+
if (lastDataSourceRef.current === td) return
|
|
115
|
+
|
|
116
|
+
lastDataSourceRef.current = td
|
|
117
|
+
_setTableData(runSideEffects(td))
|
|
104
118
|
}
|
|
105
|
-
const setTableData = td => _setTableData(runSideEffects(td))
|
|
106
119
|
|
|
107
120
|
const dispatch = useContext(EditorDispatchContext)
|
|
108
121
|
|
|
109
122
|
const fetchDatasetData = async (datasetKey, datasetConfig) => {
|
|
110
123
|
if (datasetConfig.preview) {
|
|
111
124
|
if (datasetConfig.dataUrl) {
|
|
112
|
-
const remoteData = await fetchRemoteData(datasetConfig.dataUrl)
|
|
125
|
+
const { data: remoteData } = await fetchRemoteData(datasetConfig.dataUrl)
|
|
113
126
|
if (Array.isArray(remoteData)) {
|
|
114
127
|
setTableData(remoteData)
|
|
115
128
|
}
|
|
@@ -132,7 +145,7 @@ const PreviewDataTable = () => {
|
|
|
132
145
|
await handleDashboardData(config.datasets)
|
|
133
146
|
} else {
|
|
134
147
|
if (config.dataUrl) {
|
|
135
|
-
const remoteData = await fetchRemoteData(config.dataUrl)
|
|
148
|
+
const { data: remoteData } = await fetchRemoteData(config.dataUrl)
|
|
136
149
|
if (Array.isArray(remoteData)) {
|
|
137
150
|
setTableData(remoteData)
|
|
138
151
|
}
|
|
@@ -144,16 +157,11 @@ const PreviewDataTable = () => {
|
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
loadData()
|
|
147
|
-
}, [config, config.
|
|
160
|
+
}, [config.data, config.dataUrl, config.datasets, config.type, previewData]) // eslint-disable-line
|
|
148
161
|
|
|
149
162
|
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
|
-
}
|
|
163
|
+
if (!Array.isArray(tableData)) return []
|
|
164
|
+
const columns = Object.keys(tableData[0] ?? {})
|
|
157
165
|
|
|
158
166
|
return columns.map(columnName => {
|
|
159
167
|
const columnConfig = {
|
|
@@ -166,27 +174,13 @@ const PreviewDataTable = () => {
|
|
|
166
174
|
})
|
|
167
175
|
}, [tableData])
|
|
168
176
|
|
|
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
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!tableData) return
|
|
179
|
+
const columns = Object.keys(tableData[0] ?? {})
|
|
180
|
+
if (columns.length > 0 && columns.includes('') && !errors?.includes(errorMessages.emptyCols)) {
|
|
181
|
+
dispatch({ type: 'EDITOR_SET_ERRORS', payload: [errorMessages.emptyCols] })
|
|
188
182
|
}
|
|
189
|
-
}
|
|
183
|
+
}, [dispatch, errors, tableData])
|
|
190
184
|
|
|
191
185
|
const {
|
|
192
186
|
getTableProps,
|
|
@@ -245,7 +239,8 @@ const PreviewDataTable = () => {
|
|
|
245
239
|
)
|
|
246
240
|
}
|
|
247
241
|
|
|
248
|
-
if (!tableData)
|
|
242
|
+
if (!Array.isArray(tableData) || tableData.length === 0)
|
|
243
|
+
return [<Header key='header' />, <PlaceholderTable key='table' />]
|
|
249
244
|
|
|
250
245
|
const footerProps = {
|
|
251
246
|
previousPage,
|