@cdc/core 4.25.11 → 4.26.2
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/.claude/agents/qa-test-developer.md +126 -0
- package/CLAUDE.local.md +67 -0
- package/_stories/Gallery.Charts.stories.tsx +300 -0
- package/_stories/Gallery.DataBite.stories.tsx +79 -0
- package/_stories/Gallery.Maps.stories.tsx +239 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +193 -0
- package/_stories/PageBRFSS.stories.tsx +294 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
- package/_stories/PageMaternalMortality.stories.tsx +193 -0
- package/_stories/PageOralHealth.stories.tsx +201 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +200 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
- package/_stories/PageWastewater.stories.tsx +477 -0
- package/_stories/VegaImport.stories.tsx +401 -0
- package/_stories/vega-fixtures/bars-with-line.json +444 -0
- package/_stories/vega-fixtures/bars.json +58 -0
- package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
- package/_stories/vega-fixtures/combo.json +68 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
- package/_stories/vega-fixtures/horizontal-bar.json +427 -0
- package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
- package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
- package/_stories/vega-fixtures/lines.json +227 -0
- package/_stories/vega-fixtures/measles-bars.json +348 -0
- package/_stories/vega-fixtures/measles-map.json +11101 -0
- package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
- package/_stories/vega-fixtures/multi-dataset.json +255 -0
- package/_stories/vega-fixtures/no-data.json +14 -0
- package/_stories/vega-fixtures/pie-chart.json +94 -0
- package/_stories/vega-fixtures/repeat-spec.json +47 -0
- package/_stories/vega-fixtures/stacked-area.json +222 -0
- package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
- package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
- package/_stories/vega-fixtures/stacked-bars.json +212 -0
- package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
- package/_stories/vega-fixtures/warning-combo.json +59 -0
- package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
- package/assets/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
- package/components/ComboBox/ComboBox.tsx +345 -0
- package/components/ComboBox/combobox.styles.css +185 -0
- package/components/ComboBox/index.ts +1 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/data-table.css +216 -215
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +51 -25
- package/components/EditorPanel/EditorPanel.styles.css +16 -0
- package/components/EditorPanel/EditorPanel.tsx +144 -0
- package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
- package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
- package/components/EditorPanel/Inputs.tsx +33 -7
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +31 -5
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
- package/components/Layout/components/Visualization/index.tsx +27 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/MediaControls.tsx +53 -28
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- package/components/ui/Icon.tsx +3 -1
- package/components/ui/Title/index.tsx +30 -2
- package/components/ui/Title/title.styles.css +42 -0
- package/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +235 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2803 -4448
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +118 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/addValuesToFilters.ts +6 -1
- package/helpers/cove/date.ts +33 -1
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +21 -12
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +158 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +21 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/metrics/helpers.ts +1 -0
- package/helpers/metrics/types.ts +3 -0
- package/helpers/palettes/colorDistributions.ts +1 -1
- package/helpers/palettes/utils.ts +12 -12
- package/helpers/parseCsvWithQuotes.ts +15 -14
- package/helpers/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +288 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/date.test.ts +64 -0
- package/helpers/tests/prepareScreenshot.test.ts +414 -0
- package/helpers/tests/queryStringUtils.test.ts +381 -0
- package/helpers/tests/testStandaloneBuild.ts +23 -5
- package/helpers/useDataVizClasses.ts +0 -1
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/ver/4.26.2.ts +84 -0
- package/helpers/ver/tests/4.26.1.test.ts +105 -0
- package/helpers/ver/tests/4.26.2.test.ts +298 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useDataColumns.ts +63 -0
- package/hooks/useFilterManagement.ts +94 -0
- package/hooks/useLegendSeparators.ts +26 -0
- package/hooks/useListManagement.ts +192 -0
- package/package.json +29 -33
- package/styles/_button-section.scss +0 -3
- package/styles/v2/components/editor.scss +9 -9
- package/styles/v2/utils/_grid.scss +8 -3
- package/types/Annotation.ts +10 -11
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/General.ts +2 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/Palette.ts +21 -0
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +7 -0
- package/types/VizFilter.ts +1 -0
- package/LICENSE +0 -201
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
|
@@ -195,7 +195,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
195
195
|
updateField={updateField}
|
|
196
196
|
/>
|
|
197
197
|
)}
|
|
198
|
-
{config?.visualizationType !== 'Sankey' && (
|
|
198
|
+
{config?.visualizationType !== 'Sankey' && config?.visualizationType !== 'Warming Stripes' && (
|
|
199
199
|
<label onClick={e => e.preventDefault()}>
|
|
200
200
|
<span className='edit-label column-heading mt-1'>Exclude Columns </span>
|
|
201
201
|
<MultiSelect
|
|
@@ -283,11 +283,35 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
283
283
|
<CheckBox
|
|
284
284
|
value={config.table.showDownloadImgButton}
|
|
285
285
|
fieldName='showDownloadImgButton'
|
|
286
|
-
label='Display Image
|
|
286
|
+
label='Display Image Download Link'
|
|
287
287
|
section='table'
|
|
288
288
|
updateField={updateField}
|
|
289
289
|
/>
|
|
290
290
|
)}
|
|
291
|
+
{config.type !== 'table' && config.table.showDownloadImgButton && (
|
|
292
|
+
<CheckBox
|
|
293
|
+
value={config.table.includeContextInDownload}
|
|
294
|
+
fieldName='includeContextInDownload'
|
|
295
|
+
className='ms-4'
|
|
296
|
+
label='Include Heading & Context'
|
|
297
|
+
section='table'
|
|
298
|
+
updateField={updateField}
|
|
299
|
+
tooltip={
|
|
300
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
301
|
+
<Tooltip.Target>
|
|
302
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
303
|
+
</Tooltip.Target>
|
|
304
|
+
<Tooltip.Content>
|
|
305
|
+
<p>
|
|
306
|
+
When enabled, the image download will include the section heading (H2 or H3) and any explanatory
|
|
307
|
+
paragraphs that appear immediately before the visualization. Be sure to test the image download on the
|
|
308
|
+
published page to ensure the correct context is included.
|
|
309
|
+
</p>
|
|
310
|
+
</Tooltip.Content>
|
|
311
|
+
</Tooltip>
|
|
312
|
+
}
|
|
313
|
+
/>
|
|
314
|
+
)}
|
|
291
315
|
<label>
|
|
292
316
|
<span className='edit-label column-heading'>Table Cell Min Width</span>
|
|
293
317
|
<input
|
|
@@ -296,7 +320,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
296
320
|
onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)}
|
|
297
321
|
/>
|
|
298
322
|
</label>
|
|
299
|
-
{config?.visualizationType !== 'Sankey' && (
|
|
323
|
+
{config?.visualizationType !== 'Sankey' && config?.visualizationType !== 'Warming Stripes' && (
|
|
300
324
|
<Select
|
|
301
325
|
value={config.table.groupBy}
|
|
302
326
|
fieldName={'groupBy'}
|
|
@@ -322,28 +346,30 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
322
346
|
}
|
|
323
347
|
/>
|
|
324
348
|
)}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<Tooltip
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
349
|
+
{config.visualizationType !== 'Warming Stripes' && (
|
|
350
|
+
<Select
|
|
351
|
+
label='Pivot Column'
|
|
352
|
+
tooltip={
|
|
353
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
354
|
+
<Tooltip.Target>
|
|
355
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
356
|
+
</Tooltip.Target>
|
|
357
|
+
<Tooltip.Content>
|
|
358
|
+
<p>Select a Column whos data values will be pivoted to Column Values.</p>
|
|
359
|
+
</Tooltip.Content>
|
|
360
|
+
</Tooltip>
|
|
361
|
+
}
|
|
362
|
+
value={config.table.pivot?.columnName}
|
|
363
|
+
options={groupPivotColumns.filter(
|
|
364
|
+
col => col !== config.table.groupBy && !(config.table.pivot?.valueColumns || []).includes(col)
|
|
365
|
+
)}
|
|
366
|
+
initial='-Select-'
|
|
367
|
+
section='table'
|
|
368
|
+
subsection='pivot'
|
|
369
|
+
fieldName='columnName'
|
|
370
|
+
updateField={updateField}
|
|
371
|
+
/>
|
|
372
|
+
)}
|
|
347
373
|
{config.table.pivot?.columnName && (
|
|
348
374
|
<label>
|
|
349
375
|
<span className='edit-label column-heading mt-1'>
|
|
@@ -421,3 +421,19 @@
|
|
|
421
421
|
.editor-toggle svg path {
|
|
422
422
|
fill: currentColor;
|
|
423
423
|
}
|
|
424
|
+
|
|
425
|
+
/* Shared styles consolidated from package-specific EditorPanel.styles.css files */
|
|
426
|
+
/* Previously duplicated in: data-bite, filtered-text, markup-include */
|
|
427
|
+
.editor-panel .condition-section :is(label) {
|
|
428
|
+
font-size: 1em;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
:is(span).edit-label {
|
|
432
|
+
margin-bottom: 0.3em;
|
|
433
|
+
display: block;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.react-tooltip {
|
|
437
|
+
position: absolute;
|
|
438
|
+
width: 250px;
|
|
439
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, ReactNode } from 'react'
|
|
2
|
+
import { cloneConfig } from '../../helpers/cloneConfig'
|
|
3
|
+
import ErrorBoundary from '../ErrorBoundary'
|
|
4
|
+
import Layout from '../Layout'
|
|
5
|
+
import './EditorPanel.styles.css'
|
|
6
|
+
|
|
7
|
+
export interface BaseEditorPanelProps<TConfig = any> {
|
|
8
|
+
config: TConfig
|
|
9
|
+
updateConfig: (config: TConfig) => void
|
|
10
|
+
loading?: boolean
|
|
11
|
+
setParentConfig?: (config: TConfig) => void
|
|
12
|
+
isDashboard?: boolean
|
|
13
|
+
title: string
|
|
14
|
+
children: (props: EditorPanelChildProps<TConfig>) => ReactNode
|
|
15
|
+
initialDisplayPanel?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface EditorPanelChildProps<TConfig = any> {
|
|
19
|
+
config: TConfig
|
|
20
|
+
updateConfig: (config: TConfig) => void
|
|
21
|
+
displayPanel: boolean
|
|
22
|
+
convertStateToConfig: () => TConfig
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Base EditorPanel component that provides shared functionality for all COVE visualization editors.
|
|
27
|
+
* Handles common patterns like panel display state, parent config syncing, and layout structure.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <EditorPanel
|
|
32
|
+
* config={config}
|
|
33
|
+
* updateConfig={updateConfig}
|
|
34
|
+
* loading={loading}
|
|
35
|
+
* setParentConfig={setParentConfig}
|
|
36
|
+
* isDashboard={isDashboard}
|
|
37
|
+
* title="Configure My Visualization"
|
|
38
|
+
* >
|
|
39
|
+
* {() => (
|
|
40
|
+
* <Accordion>
|
|
41
|
+
* <Accordion.Section title="General">
|
|
42
|
+
* // Your configuration UI here
|
|
43
|
+
* </Accordion.Section>
|
|
44
|
+
* </Accordion>
|
|
45
|
+
* )}
|
|
46
|
+
* </EditorPanel>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function EditorPanel<TConfig = any>({
|
|
50
|
+
config,
|
|
51
|
+
updateConfig,
|
|
52
|
+
loading = false,
|
|
53
|
+
setParentConfig,
|
|
54
|
+
isDashboard,
|
|
55
|
+
title,
|
|
56
|
+
children,
|
|
57
|
+
initialDisplayPanel = true
|
|
58
|
+
}: BaseEditorPanelProps<TConfig>) {
|
|
59
|
+
const [displayPanel, setDisplayPanel] = useState(initialDisplayPanel)
|
|
60
|
+
const prevConfigRef = useRef<string>()
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Converts current config to a clean state suitable for parent consumption.
|
|
64
|
+
* Removes runtime-only properties like 'newViz' and 'runtime'.
|
|
65
|
+
* In dashboard context, preserve 'editing' as it's managed by the dashboard.
|
|
66
|
+
*/
|
|
67
|
+
const convertStateToConfig = useCallback((): TConfig => {
|
|
68
|
+
const strippedState = cloneConfig(config)
|
|
69
|
+
delete strippedState.newViz
|
|
70
|
+
delete strippedState.runtime
|
|
71
|
+
// Only delete editing flag if NOT in a dashboard context
|
|
72
|
+
if (!isDashboard) {
|
|
73
|
+
delete strippedState.editing
|
|
74
|
+
}
|
|
75
|
+
return strippedState
|
|
76
|
+
}, [config, isDashboard])
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sync config changes up to parent component when setParentConfig is provided.
|
|
80
|
+
* This is typically used in dashboard/editor contexts where the parent needs to track changes.
|
|
81
|
+
* Uses ref to prevent infinite loops by only syncing when config content actually changes.
|
|
82
|
+
*/
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (setParentConfig) {
|
|
85
|
+
const strippedState = cloneConfig(config)
|
|
86
|
+
delete strippedState.newViz
|
|
87
|
+
delete strippedState.runtime
|
|
88
|
+
// Only delete editing flag if NOT in a dashboard context
|
|
89
|
+
if (!isDashboard) {
|
|
90
|
+
delete strippedState.editing
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Only call setParentConfig if the config content actually changed
|
|
94
|
+
const configString = JSON.stringify(strippedState)
|
|
95
|
+
if (prevConfigRef.current !== configString) {
|
|
96
|
+
prevConfigRef.current = configString
|
|
97
|
+
setParentConfig(strippedState)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [config, setParentConfig, isDashboard])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Toggle the editor panel visibility and update config to reflect the change.
|
|
104
|
+
* In dashboard context, also sets editing to false to return to dashboard editor.
|
|
105
|
+
*/
|
|
106
|
+
const onBackClick = () => {
|
|
107
|
+
const newDisplayPanel = !displayPanel
|
|
108
|
+
setDisplayPanel(newDisplayPanel)
|
|
109
|
+
const newConfig: TConfig = {
|
|
110
|
+
...config,
|
|
111
|
+
showEditorPanel: newDisplayPanel
|
|
112
|
+
}
|
|
113
|
+
// If in dashboard mode, set editing to false to return to dashboard editor
|
|
114
|
+
if (isDashboard) {
|
|
115
|
+
(newConfig as any).editing = false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Update local config - the useEffect will handle syncing to parent
|
|
119
|
+
updateConfig(newConfig)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Don't render if loading and panel should be hidden
|
|
123
|
+
if (loading && !(config as any)?.showEditorPanel) return null
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<ErrorBoundary component='EditorPanel'>
|
|
127
|
+
<Layout.Sidebar
|
|
128
|
+
displayPanel={displayPanel}
|
|
129
|
+
isDashboard={isDashboard || false}
|
|
130
|
+
title={title}
|
|
131
|
+
onBackClick={onBackClick}
|
|
132
|
+
>
|
|
133
|
+
{children({
|
|
134
|
+
config,
|
|
135
|
+
updateConfig,
|
|
136
|
+
displayPanel,
|
|
137
|
+
convertStateToConfig
|
|
138
|
+
})}
|
|
139
|
+
</Layout.Sidebar>
|
|
140
|
+
</ErrorBoundary>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export default EditorPanel
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
import ErrorBoundary from '../ErrorBoundary'
|
|
3
|
+
import Layout from '../Layout'
|
|
4
|
+
|
|
5
|
+
export interface EditorPanelDispatchProps<TState = any, TAction = any> {
|
|
6
|
+
state: TState
|
|
7
|
+
dispatch: React.Dispatch<TAction>
|
|
8
|
+
title: string
|
|
9
|
+
children: (props: EditorPanelDispatchChildProps<TState, TAction>) => ReactNode
|
|
10
|
+
showEditorPanelKey?: keyof TState
|
|
11
|
+
toggleActionType?: string
|
|
12
|
+
isDashboard?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface EditorPanelDispatchChildProps<TState = any, TAction = any> {
|
|
16
|
+
state: TState
|
|
17
|
+
dispatch: React.Dispatch<TAction>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Base EditorPanel component for packages using dispatch/reducer pattern (e.g., data-table)
|
|
22
|
+
*
|
|
23
|
+
* Provides common wrapper functionality including:
|
|
24
|
+
* - ErrorBoundary for error handling
|
|
25
|
+
* - Layout.Sidebar for consistent panel display
|
|
26
|
+
* - State management for panel visibility
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <EditorPanelDispatch
|
|
31
|
+
* state={state}
|
|
32
|
+
* dispatch={dispatch}
|
|
33
|
+
* title='Configure Data Table'
|
|
34
|
+
* showEditorPanelKey='showEditorPanel'
|
|
35
|
+
* toggleActionType='SET_SHOW_EDITOR_PANEL'
|
|
36
|
+
* >
|
|
37
|
+
* {({ state, dispatch }) => (
|
|
38
|
+
* <Accordion>
|
|
39
|
+
* // Your editor content here
|
|
40
|
+
* </Accordion>
|
|
41
|
+
* )}
|
|
42
|
+
* </EditorPanelDispatch>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function EditorPanelDispatch<TState = any, TAction = any>({
|
|
46
|
+
state,
|
|
47
|
+
dispatch,
|
|
48
|
+
title,
|
|
49
|
+
children,
|
|
50
|
+
showEditorPanelKey = 'showEditorPanel' as keyof TState,
|
|
51
|
+
toggleActionType = 'SET_SHOW_EDITOR_PANEL',
|
|
52
|
+
isDashboard = false
|
|
53
|
+
}: EditorPanelDispatchProps<TState, TAction>) {
|
|
54
|
+
const showEditorPanel = state[showEditorPanelKey] as boolean
|
|
55
|
+
|
|
56
|
+
const onBackClick = () => {
|
|
57
|
+
dispatch({
|
|
58
|
+
type: toggleActionType,
|
|
59
|
+
payload: !showEditorPanel
|
|
60
|
+
} as TAction)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<ErrorBoundary component='EditorPanel'>
|
|
65
|
+
<Layout.Sidebar
|
|
66
|
+
title={title}
|
|
67
|
+
onBackClick={onBackClick}
|
|
68
|
+
displayPanel={showEditorPanel}
|
|
69
|
+
isDashboard={isDashboard}
|
|
70
|
+
>
|
|
71
|
+
{children({ state, dispatch })}
|
|
72
|
+
</Layout.Sidebar>
|
|
73
|
+
</ErrorBoundary>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -9,42 +9,85 @@ type FieldSetProps = {
|
|
|
9
9
|
controls: OpenControls
|
|
10
10
|
deleteField: Function
|
|
11
11
|
children: React.ReactNode
|
|
12
|
+
draggable?: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
const FieldSet: React.FC<FieldSetProps> = ({
|
|
15
|
+
const FieldSet: React.FC<FieldSetProps> = ({
|
|
16
|
+
fieldName,
|
|
17
|
+
fieldKey,
|
|
18
|
+
fieldType,
|
|
19
|
+
controls,
|
|
20
|
+
deleteField,
|
|
21
|
+
children,
|
|
22
|
+
draggable = false
|
|
23
|
+
}) => {
|
|
15
24
|
const [openControls, setOpenControls] = controls
|
|
16
25
|
const show = openControls[fieldKey]
|
|
17
26
|
const setShow = (key, value) => {
|
|
18
27
|
setOpenControls({ ...openControls, [key]: value })
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
// Markup for non-draggable items
|
|
31
|
+
if (!draggable) {
|
|
32
|
+
if (!show)
|
|
33
|
+
return (
|
|
34
|
+
<div className='mb-1'>
|
|
35
|
+
<button type='button' className='btn btn-light' onClick={() => setShow(fieldKey, true)}>
|
|
36
|
+
<Icon display='caretDown' />
|
|
37
|
+
</button>
|
|
38
|
+
<span> {fieldName ? `${fieldName}` : 'New ' + fieldType}</span>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
22
41
|
return (
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
<fieldset className='edit-block mb-1' key={fieldKey}>
|
|
43
|
+
<div className='d-flex justify-content-between'>
|
|
44
|
+
<button type='button' className='btn btn-light' onClick={() => setShow(fieldKey, false)}>
|
|
45
|
+
<Icon display='caretUp' />
|
|
46
|
+
</button>
|
|
47
|
+
<button
|
|
48
|
+
type='button'
|
|
49
|
+
className='btn btn-danger btn-sm'
|
|
50
|
+
onClick={event => {
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
deleteField()
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
Remove
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
{children}
|
|
59
|
+
</fieldset>
|
|
29
60
|
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Draggable fieldset
|
|
30
64
|
return (
|
|
31
|
-
<
|
|
32
|
-
<div className='
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<button
|
|
37
|
-
className='btn btn-danger btn-sm'
|
|
38
|
-
onClick={event => {
|
|
39
|
-
event.preventDefault()
|
|
40
|
-
deleteField()
|
|
41
|
-
}}
|
|
42
|
-
>
|
|
43
|
-
Remove
|
|
65
|
+
<div className='editor-field-item'>
|
|
66
|
+
<div className='editor-field-item__header'>
|
|
67
|
+
<Icon display='move' size={15} style={{ marginRight: '0.5rem' }} />
|
|
68
|
+
<button type='button' className='btn btn-light' onClick={() => setShow(fieldKey, !show)}>
|
|
69
|
+
<Icon display={show ? 'caretUp' : 'caretDown'} size={20} />
|
|
44
70
|
</button>
|
|
71
|
+
<span className='editor-field-item__name'>{fieldName ? `${fieldName}` : 'New ' + fieldType}</span>
|
|
45
72
|
</div>
|
|
46
|
-
{
|
|
47
|
-
|
|
73
|
+
{show && (
|
|
74
|
+
<div className='editor-field-item__content'>
|
|
75
|
+
<div className='editor-field-item__remove-wrapper'>
|
|
76
|
+
<button
|
|
77
|
+
type='button'
|
|
78
|
+
className='btn btn-danger btn-sm'
|
|
79
|
+
onClick={event => {
|
|
80
|
+
event.preventDefault()
|
|
81
|
+
deleteField()
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
Remove
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
{children}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
48
91
|
)
|
|
49
92
|
}
|
|
50
93
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useEffect, useState } from 'react'
|
|
1
|
+
import { memo, useEffect, useState, useMemo } from 'react'
|
|
2
2
|
import { useDebounce } from 'use-debounce'
|
|
3
3
|
import { DROPDOWN_STYLES } from '../Filters/components/Dropdown'
|
|
4
4
|
|
|
@@ -50,6 +50,13 @@ const TextField = memo((props: TextFieldProps) => {
|
|
|
50
50
|
const [value, setValue] = useState(stateValue)
|
|
51
51
|
const [debouncedValue] = useDebounce(value, 500)
|
|
52
52
|
|
|
53
|
+
// Generate unique ID for accessibility
|
|
54
|
+
const inputId = useMemo(() => {
|
|
55
|
+
const sectionPart = section ?? 'root'
|
|
56
|
+
const subsectionPart = subsection ?? 'none'
|
|
57
|
+
return attributes.id || `input-${sectionPart}-${subsectionPart}-${fieldName}`
|
|
58
|
+
}, [section, subsection, fieldName, attributes.id])
|
|
59
|
+
|
|
53
60
|
useEffect(() => {
|
|
54
61
|
if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
|
|
55
62
|
updateField(section, subsection, fieldName, debouncedValue, i)
|
|
@@ -74,25 +81,25 @@ const TextField = memo((props: TextFieldProps) => {
|
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
let formElement = <input type='text' name={name} onChange={onChange} {...attributes} value={value} />
|
|
84
|
+
let formElement = <input type='text' id={inputId} name={name} onChange={onChange} {...attributes} value={value} />
|
|
78
85
|
|
|
79
86
|
if ('textarea' === type) {
|
|
80
|
-
formElement = <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
|
|
87
|
+
formElement = <textarea id={inputId} name={name} onChange={onChange} {...attributes} value={value}></textarea>
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
if ('number' === type) {
|
|
84
|
-
formElement = <input type='number' name={name} onChange={onChange} {...attributes} value={value} />
|
|
91
|
+
formElement = <input type='number' id={inputId} name={name} onChange={onChange} {...attributes} value={value} />
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
if ('date' === type) {
|
|
88
|
-
formElement = <input type='date' name={name} onChange={onChange} {...attributes} value={value} />
|
|
95
|
+
formElement = <input type='date' id={inputId} name={name} onChange={onChange} {...attributes} value={value} />
|
|
89
96
|
}
|
|
90
97
|
if (!display) {
|
|
91
98
|
return <></>
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
return (
|
|
95
|
-
<label>
|
|
102
|
+
<label htmlFor={inputId}>
|
|
96
103
|
<span className='edit-label column-heading'>
|
|
97
104
|
{label}
|
|
98
105
|
{tooltip}
|
|
@@ -114,11 +121,20 @@ const CheckBox = memo((props: CheckboxProps) => {
|
|
|
114
121
|
updateField,
|
|
115
122
|
...attributes
|
|
116
123
|
} = props
|
|
124
|
+
|
|
125
|
+
// Generate unique ID for accessibility
|
|
126
|
+
const inputId = useMemo(() => {
|
|
127
|
+
const sectionPart = section ?? 'root'
|
|
128
|
+
const subsectionPart = subsection ?? 'none'
|
|
129
|
+
return attributes.id || `checkbox-${sectionPart}-${subsectionPart}-${fieldName}`
|
|
130
|
+
}, [section, subsection, fieldName, attributes.id])
|
|
131
|
+
|
|
117
132
|
if (!display) {
|
|
118
133
|
return <></>
|
|
119
134
|
}
|
|
120
135
|
return (
|
|
121
136
|
<label
|
|
137
|
+
htmlFor={inputId}
|
|
122
138
|
className='checkbox column-heading'
|
|
123
139
|
onClick={e => {
|
|
124
140
|
if (!['SPAN', 'INPUT'].includes(e.target.nodeName)) {
|
|
@@ -128,6 +144,7 @@ const CheckBox = memo((props: CheckboxProps) => {
|
|
|
128
144
|
>
|
|
129
145
|
<input
|
|
130
146
|
type='checkbox'
|
|
147
|
+
id={inputId}
|
|
131
148
|
className='edit-checkbox'
|
|
132
149
|
name={fieldName}
|
|
133
150
|
checked={value}
|
|
@@ -172,6 +189,14 @@ const Select = memo((props: SelectProps) => {
|
|
|
172
189
|
onChange: onChangeProp,
|
|
173
190
|
...attributes
|
|
174
191
|
} = props
|
|
192
|
+
|
|
193
|
+
// Generate unique ID for accessibility
|
|
194
|
+
const inputId = useMemo(() => {
|
|
195
|
+
const sectionPart = section ?? 'root'
|
|
196
|
+
const subsectionPart = subsection ?? 'none'
|
|
197
|
+
return attributes.id || `select-${sectionPart}-${subsectionPart}-${fieldName}`
|
|
198
|
+
}, [section, subsection, fieldName, attributes.id])
|
|
199
|
+
|
|
175
200
|
const optionsJsx = options?.map((option, index) => {
|
|
176
201
|
if (typeof option === 'string') {
|
|
177
202
|
return (
|
|
@@ -200,12 +225,13 @@ const Select = memo((props: SelectProps) => {
|
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
return (
|
|
203
|
-
<label style={disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}}>
|
|
228
|
+
<label htmlFor={inputId} style={disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}}>
|
|
204
229
|
<span className='edit-label'>
|
|
205
230
|
{label}
|
|
206
231
|
{tooltip}
|
|
207
232
|
</span>
|
|
208
233
|
<select
|
|
234
|
+
id={inputId}
|
|
209
235
|
className={`cove-form-select ${required && !value ? 'warning' : ''} ${DROPDOWN_STYLES}`}
|
|
210
236
|
name={fieldName}
|
|
211
237
|
value={value}
|
|
@@ -16,6 +16,7 @@ type NestedDropdownEditorProps = {
|
|
|
16
16
|
updateField: Function
|
|
17
17
|
updateFilterStyle: Function
|
|
18
18
|
handleGroupingCustomOrder: (index1: number, index2: number) => void
|
|
19
|
+
onNestedDragAreaHover?: (isHovering: boolean) => void
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
@@ -25,7 +26,8 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
25
26
|
handleNameChange: handleGroupColumnNameChange,
|
|
26
27
|
filterIndex,
|
|
27
28
|
rawData,
|
|
28
|
-
updateField
|
|
29
|
+
updateField,
|
|
30
|
+
onNestedDragAreaHover
|
|
29
31
|
}) => {
|
|
30
32
|
const filter = config.filters[filterIndex]
|
|
31
33
|
const subGrouping = filter?.subGrouping
|
|
@@ -159,7 +161,10 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
159
161
|
<Select
|
|
160
162
|
label='Filter Grouping'
|
|
161
163
|
value={filter.columnName}
|
|
162
|
-
options={[
|
|
164
|
+
options={[
|
|
165
|
+
{ value: '', label: '- Select Option -' },
|
|
166
|
+
...columnNameOptions.map(opt => ({ value: opt, label: opt }))
|
|
167
|
+
]}
|
|
163
168
|
onChange={e => handleGroupColumnNameChange(e.target.value)}
|
|
164
169
|
/>
|
|
165
170
|
|
|
@@ -168,9 +173,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
168
173
|
value={subGrouping?.columnName ?? ''}
|
|
169
174
|
options={[
|
|
170
175
|
{ value: '', label: '- Select Option -' },
|
|
171
|
-
...columnNameOptions
|
|
172
|
-
.filter(option => option !== filter.columnName)
|
|
173
|
-
.map(opt => ({ value: opt, label: opt }))
|
|
176
|
+
...columnNameOptions.filter(option => option !== filter.columnName).map(opt => ({ value: opt, label: opt }))
|
|
174
177
|
]}
|
|
175
178
|
onChange={e => {
|
|
176
179
|
handleSubGroupColumnNameChange(e.target.value)
|
|
@@ -222,7 +225,11 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
222
225
|
onChange={e => handleGroupingOrderBy(e.target.value as OrderBy)}
|
|
223
226
|
/>
|
|
224
227
|
{filter.order === 'cust' && (
|
|
225
|
-
<FilterOrder
|
|
228
|
+
<FilterOrder
|
|
229
|
+
orderedValues={filter.orderedValues}
|
|
230
|
+
handleFilterOrder={handleGroupingCustomOrder}
|
|
231
|
+
onNestedDragAreaHover={onNestedDragAreaHover}
|
|
232
|
+
/>
|
|
226
233
|
)}
|
|
227
234
|
</div>
|
|
228
235
|
|
|
@@ -247,6 +254,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
247
254
|
handleFilterOrder={(sourceIndex, destinationIndex) => {
|
|
248
255
|
handleSubGroupingCustomOrder(sourceIndex, destinationIndex, orderedSubGroupValues, groupName)
|
|
249
256
|
}}
|
|
257
|
+
onNestedDragAreaHover={onNestedDragAreaHover}
|
|
250
258
|
/>
|
|
251
259
|
</div>
|
|
252
260
|
)
|