@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
@@ -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, useCallback, useEffect, memo } from 'react'
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 Icon from '@cdc/core/components/ui/Icon'
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 && debouncedValue !== globalFilter) {
30
- setGlobalFilter(debouncedValue ?? '')
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
- <button
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
- </button>
73
+ </Button>
70
74
  </li>
71
75
  <li className='me-2'>
72
- <button
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
- </button>
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
- return Object.values(config.datasets).find((dataset: DataSet) => {
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 newData = generateColumns(isSankey ? td[0].tableData : td)
102
- validateFipsCodeLength(newData)
103
- return newData
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.data, previewData]) // eslint-disable-line
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.columns ?? []
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
- // This adds a columns property just like the D3 function for JSON parsing.
170
- const generateColumns = data => {
171
- let columns = []
172
-
173
- data.forEach(rowObj => {
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) return [<Header key='header' />, <PlaceholderTable key='table' />]
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
- <div className='btn btn-inline' onClick={props.onCancel}>
9
+ <Button type='button' className='btn btn-inline' onClick={props.onCancel}>
9
10
  No
10
- </div>
11
- <div className='btn btn-inline' onClick={props.onConfirm}>
11
+ </Button>
12
+ <Button type='button' className='btn btn-inline' onClick={props.onConfirm}>
12
13
  Yes
13
- </div>
14
+ </Button>
14
15
  </div>
15
16
  </>
16
17
  )
@@ -1,6 +1,6 @@
1
1
  @import 'variables';
2
2
 
3
- .cdc-open-viz-module.cdc-editor {
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: 0.1s all;
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;