@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.
Files changed (30) hide show
  1. package/dist/cdceditor-CY9IcPSi.es.js +6 -0
  2. package/dist/cdceditor-DlpiY3fQ.es.js +4 -0
  3. package/dist/cdceditor.js +84436 -77912
  4. package/package.json +9 -9
  5. package/src/CdcEditor.tsx +2 -3
  6. package/src/_stories/Editor.stories.tsx +173 -6
  7. package/src/components/ChooseTab.test.tsx +36 -0
  8. package/src/components/ChooseTab.tsx +39 -26
  9. package/src/components/DataImport/components/DataImport.tsx +124 -35
  10. package/src/components/DataImport/helpers/applyAutoDetectedDateParseFormat.ts +35 -0
  11. package/src/components/DataImport/tests/applyAutoDetectedDateParseFormat.test.ts +128 -0
  12. package/src/components/PreviewDataTable.test.tsx +184 -0
  13. package/src/components/PreviewDataTable.tsx +55 -49
  14. package/src/components/modal/Confirmation.jsx +5 -4
  15. package/src/scss/main.scss +15 -2
  16. package/dist/cdceditor-Cf9_fbQf.es.js +0 -6
  17. package/example/data-horizontal-filters.json +0 -8
  18. package/example/data-horizontal-multiseries-filters.json +0 -18
  19. package/example/data-horizontal-multiseries.json +0 -6
  20. package/example/data-horizontal.json +0 -4
  21. package/example/data-vertical-filters.json +0 -10
  22. package/example/data-vertical-multiseries-filters.json +0 -18
  23. package/example/data-vertical-multiseries-multirow-filters.json +0 -50
  24. package/example/data-vertical-multiseries-multirow.json +0 -14
  25. package/example/data-vertical-multiseries.json +0 -6
  26. package/example/data-vertical.json +0 -6
  27. package/example/region-map.json +0 -33
  28. package/example/test.json +0 -110280
  29. package/example/valid-county-data.json +0 -3049
  30. package/example/valid-scatterplot.csv +0 -17
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@cdc/editor",
3
- "version": "4.26.2",
3
+ "version": "4.26.4",
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.2",
9
- "@cdc/core": "^4.26.2",
10
- "@cdc/dashboard": "^4.26.2",
11
- "@cdc/data-bite": "^4.26.2",
12
- "@cdc/map": "^4.26.2",
13
- "@cdc/markup-include": "^4.26.2",
14
- "@cdc/waffle-chart": "^4.26.2",
8
+ "@cdc/chart": "^4.26.4",
9
+ "@cdc/core": "^4.26.4",
10
+ "@cdc/dashboard": "^4.26.4",
11
+ "@cdc/data-bite": "^4.26.4",
12
+ "@cdc/map": "^4.26.4",
13
+ "@cdc/markup-include": "^4.26.4",
14
+ "@cdc/waffle-chart": "^4.26.4",
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": "be3413e8e1149abf94225108f86a7910f56e0616",
26
+ "gitHead": "6097de1ff814001880d9ac64bd66becdc092d63c",
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={`cdc-open-viz-module cdc-editor ${state.currentViewport}`} ref={outerContainerRef}>
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 sleep = ms => {
6
- return new Promise(r => setTimeout(r, ms))
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,157 @@ export const PreviewTableTests: Story = {
26
46
  play: async ({ canvasElement }) => {
27
47
  const canvas = within(canvasElement)
28
48
  const user = userEvent.setup()
29
- await sleep(1000)
30
- const mapButton = canvas.getByText('United States (State- or County-Level)')
49
+
50
+ const mapButton = await canvas.findByRole('button', { name: 'United States (State- or County-Level)' })
31
51
  await user.click(mapButton)
32
- const sampleData = canvas.getByText('United States: County Sample Data')
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 DownloadDashboardDatasetCSV: Story = {
107
+ args: { config: {} },
108
+ play: async ({ canvasElement }) => {
109
+ const canvas = within(canvasElement)
110
+ const user = userEvent.setup()
111
+
112
+ await loadConfigFromTextArea(canvasElement, DashboardConfig)
113
+ await user.click(canvas.getByText('2. Import Data'))
114
+ await expect(canvas.findByText('Data Sources')).resolves.toBeTruthy()
115
+
116
+ const originalCreateObjectURL = URL.createObjectURL
117
+ let capturedBlob: Blob | null = null
118
+ URL.createObjectURL = (blob: Blob) => {
119
+ capturedBlob = blob
120
+ return 'blob:test-mock'
121
+ }
122
+
123
+ try {
124
+ const downloadBtn = await canvas.findByRole('button', { name: 'Download' })
125
+ await user.click(downloadBtn)
126
+
127
+ expect(capturedBlob).toBeTruthy()
128
+ const text = await capturedBlob!.text()
129
+ expect(text).toContain('Location')
130
+ expect(text).toContain('Rate')
131
+ } finally {
132
+ URL.createObjectURL = originalCreateObjectURL
133
+ }
134
+ }
135
+ }
136
+
137
+ export const DownloadSingleVizCSV: Story = {
138
+ args: { config: {} },
139
+ play: async ({ canvasElement }) => {
140
+ const canvas = within(canvasElement)
141
+ const user = userEvent.setup()
142
+
143
+ await loadConfigFromTextArea(canvasElement, ChartEditorConfig)
144
+ await user.click(canvas.getByText('2. Import Data'))
145
+ await expect(canvas.findByText('Data Preview')).resolves.toBeTruthy()
146
+
147
+ const originalCreateObjectURL = URL.createObjectURL
148
+ let capturedBlob: Blob | null = null
149
+ URL.createObjectURL = (blob: Blob) => {
150
+ capturedBlob = blob
151
+ return 'blob:test-mock'
152
+ }
153
+
154
+ try {
155
+ const downloadBtn = await canvas.findByRole('button', { name: 'Download CSV' })
156
+ await user.click(downloadBtn)
157
+
158
+ expect(capturedBlob).toBeTruthy()
159
+ const text = await capturedBlob!.text()
160
+ expect(text).toContain('Year')
161
+ expect(text).toContain('Category')
162
+ } finally {
163
+ URL.createObjectURL = originalCreateObjectURL
164
+ }
165
+ }
166
+ }
167
+
168
+ export const InvalidJsonShowsValidationAlert: Story = {
169
+ args: {
170
+ config: {}
171
+ },
172
+ play: async ({ canvasElement }) => {
173
+ const user = userEvent.setup()
174
+ const textArea = canvasElement.querySelector('#pasteConfig') as HTMLTextAreaElement
175
+ const loadButton = canvasElement.querySelector('#load-data') as HTMLButtonElement
176
+
177
+ expect(textArea).toBeTruthy()
178
+ expect(loadButton).toBeTruthy()
179
+
180
+ const originalAlert = window.alert
181
+ const originalOnError = window.onerror
182
+ let alertText = ''
183
+
184
+ window.alert = message => {
185
+ alertText = String(message)
186
+ }
187
+
188
+ window.onerror = () => true
189
+
190
+ try {
191
+ await user.click(textArea)
192
+ await user.clear(textArea)
193
+ await user.paste('{"broken": true, }')
194
+ await user.click(loadButton)
195
+
196
+ await expect(alertText).toBe('The JSON that was entered is invalid.')
197
+ } finally {
198
+ window.alert = originalAlert
199
+ window.onerror = originalOnError
200
+ }
34
201
  }
35
202
  }
@@ -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,8 +1,9 @@
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'
5
5
  import Tooltip from '@cdc/core/components/ui/Tooltip'
6
+ import Button from '@cdc/core/components/elements/Button'
6
7
 
7
8
  import AlabamaGraphic from '@cdc/core/assets/icon-map-alabama.svg'
8
9
  import AreaChartIcon from '@cdc/core/assets/icon-area-chart.svg'
@@ -51,6 +52,23 @@ interface ButtonProps {
51
52
  orientation?: string | null
52
53
  }
53
54
 
55
+ interface VizButtonProps extends ButtonProps {
56
+ activeVizButtonID?: number
57
+ onConfigure: (props: Record<string, unknown>) => void
58
+ }
59
+
60
+ const VizButton: React.FC<VizButtonProps> = ({ activeVizButtonID, onConfigure, ...buttonProps }) => {
61
+ const { label, icon, id } = buttonProps
62
+ const isActive = id === activeVizButtonID || 0
63
+
64
+ return (
65
+ <button className={isActive ? 'active' : ''} onClick={() => onConfigure(buttonProps)} aria-label={label}>
66
+ {icon}
67
+ <span className='mt-1'>{label}</span>
68
+ </button>
69
+ )
70
+ }
71
+
54
72
  const ChooseTab: React.FC = (): JSX.Element => {
55
73
  const { config, tempConfig } = useContext(ConfigContext)
56
74
 
@@ -59,6 +77,12 @@ const ChooseTab: React.FC = (): JSX.Element => {
59
77
  const dispatch = useContext(EditorDispatchContext)
60
78
  const rowLabels = ['General', , 'Charts', 'Maps']
61
79
 
80
+ useEffect(() => {
81
+ if (tempConfig) {
82
+ dispatch({ type: 'EDITOR_SAVE', payload: tempConfig })
83
+ }
84
+ }, [dispatch, tempConfig])
85
+
62
86
  const handleUpload = e => {
63
87
  const file = e.target.files[0]
64
88
  const reader = new FileReader()
@@ -75,12 +99,15 @@ const ChooseTab: React.FC = (): JSX.Element => {
75
99
  newConfig = JSON.parse(text)
76
100
  } catch (e) {
77
101
  alert('The JSON that was entered is invalid.')
78
- throw new Error()
102
+ return
79
103
  }
80
104
 
81
105
  const isVega = isVegaConfig(newConfig)
82
106
  if (isVega) {
83
107
  newConfig = importVegaConfig(newConfig)
108
+ if (!newConfig) {
109
+ return
110
+ }
84
111
  }
85
112
 
86
113
  dispatch({ type: 'EDITOR_SET_CONFIG', payload: newConfig })
@@ -112,7 +139,7 @@ const ChooseTab: React.FC = (): JSX.Element => {
112
139
 
113
140
  const errorText = vegaErrors.join('\n\n')
114
141
  alert(errorText)
115
- throw new Error(errorText)
142
+ return null
116
143
  }
117
144
 
118
145
  const generateNewConfig = props => {
@@ -174,25 +201,6 @@ const ChooseTab: React.FC = (): JSX.Element => {
174
201
  dispatch({ type: 'EDITOR_SET_GLOBALACTIVE', payload: 1 })
175
202
  }
176
203
 
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
204
  return (
197
205
  <div className='choose-vis'>
198
206
  <a
@@ -221,7 +229,11 @@ const ChooseTab: React.FC = (): JSX.Element => {
221
229
  <li key={`${label}-button-${buttonIndex}`}>
222
230
  <Tooltip position='right'>
223
231
  <Tooltip.Target>
224
- <VizButton {...button} />
232
+ <VizButton
233
+ {...button}
234
+ activeVizButtonID={config?.activeVizButtonID}
235
+ onConfigure={configureTabs}
236
+ />
225
237
  </Tooltip.Target>
226
238
  <Tooltip.Content>{button.content}</Tooltip.Content>
227
239
  </Tooltip>
@@ -263,15 +275,16 @@ const ChooseTab: React.FC = (): JSX.Element => {
263
275
  placeholder='{ }'
264
276
  value={pastedConfig}
265
277
  />
266
- <button
267
- className='btn btn-primary px-4 ms-2'
278
+ <Button
279
+ variant='primary'
280
+ className='px-4 ms-2'
268
281
  type='submit'
269
282
  id='load-data'
270
283
  disabled={!pastedConfig}
271
284
  onClick={() => importConfig(pastedConfig)}
272
285
  >
273
286
  Load
274
- </button>
287
+ </Button>
275
288
  </div>
276
289
  </div>
277
290
  </div>