@cdc/core 4.25.7 → 4.25.10
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/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
- package/components/DataTable/DataTable.tsx +63 -11
- package/components/DataTable/DataTableStandAlone.tsx +4 -1
- package/components/DataTable/components/ChartHeader.tsx +58 -9
- package/components/DataTable/components/ExpandCollapse.tsx +21 -1
- package/components/DataTable/components/MapHeader.tsx +35 -7
- package/components/DataTable/data-table.css +6 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
- package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
- package/components/DownloadButton.tsx +42 -13
- package/components/EditorPanel/DataTableEditor.tsx +10 -1
- package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
- package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
- package/components/ErrorBoundary.jsx +3 -1
- package/components/Filters/Filters.tsx +35 -11
- package/components/Filters/components/Tabs.tsx +1 -0
- package/components/Footnotes/FootnotesStandAlone.tsx +2 -1
- package/components/Legend/Legend.Gradient.tsx +3 -6
- package/components/LegendShape.tsx +121 -3
- package/components/{MediaControls.jsx → MediaControls.tsx} +80 -16
- package/components/PaletteConversionModal.tsx +87 -0
- package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
- package/components/PaletteSelector/PaletteSelector.css +51 -0
- package/components/PaletteSelector/PaletteSelector.tsx +112 -0
- package/components/PaletteSelector/index.ts +2 -0
- package/components/RichTooltip/RichTooltip.tsx +1 -0
- package/components/Table/Table.tsx +3 -1
- package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
- package/components/_stories/DataTable.stories.tsx +1 -1
- package/components/_stories/Filters.stories.tsx +1 -1
- package/components/_stories/Footnotes.stories.tsx +1 -1
- package/components/_stories/Inputs.stories.tsx +1 -1
- package/components/_stories/MultiSelect.stories.tsx +3 -3
- package/components/_stories/NestedDropdown.stories.tsx +1 -1
- package/components/_stories/Table.stories.tsx +1 -1
- package/components/elements/_stories/Button.stories.tsx +1 -1
- package/components/elements/_stories/Card.stories.tsx +1 -1
- package/components/inputs/InputToggle.tsx +2 -0
- package/components/managers/DataDesigner.tsx +10 -9
- package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
- package/components/ui/Tooltip.tsx +2 -1
- package/components/ui/_stories/Accordion.stories.tsx +1 -1
- package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
- package/components/ui/_stories/Colors.stories.tsx +330 -0
- package/components/ui/_stories/IconGallery.stories.tsx +316 -0
- package/components/ui/_stories/Title.stories.tsx +1 -1
- package/contexts/EditorContext.ts +18 -0
- package/contexts/editor.actions.ts +28 -0
- package/contexts/editor.reducer.ts +94 -0
- package/data/chartColorPalettes.ts +118 -0
- package/data/colorPalettes.ts +9 -0
- package/data/mapColorPalettes.ts +45 -0
- package/data/sharedPalettes.ts +50 -0
- package/dist/cove-main.css +14 -13
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +80 -0
- package/helpers/addValuesToFilters.ts +2 -3
- package/helpers/cloneConfig.ts +31 -0
- package/helpers/configDataHelpers.ts +128 -0
- package/helpers/configHelpers.ts +27 -0
- package/helpers/constants.ts +5 -2
- package/helpers/cove/number.ts +6 -2
- package/helpers/coveUpdateWorker.ts +15 -3
- package/helpers/events.ts +32 -0
- package/helpers/filterColorPalettes.ts +152 -0
- package/helpers/generateColorsArray.ts +13 -0
- package/helpers/getColorPaletteVersion.ts +33 -0
- package/helpers/getPaletteAccessor.ts +18 -0
- package/helpers/markupProcessor.ts +205 -0
- package/helpers/metrics/helpers.ts +75 -0
- package/helpers/metrics/types.ts +82 -0
- package/helpers/metrics/utils.ts +34 -0
- package/helpers/palettes/colorDistributions.ts +56 -0
- package/helpers/palettes/migratePaletteName.ts +150 -0
- package/helpers/palettes/standardizePaletteNames.ts +77 -0
- package/helpers/palettes/utils.ts +267 -0
- package/helpers/queryStringUtils.ts +13 -0
- package/helpers/testing.ts +345 -0
- package/helpers/tests/addValuesToFilters.test.ts +1 -2
- package/helpers/tests/generateColorsArray.test.ts +24 -0
- package/helpers/tests/markupProcessor.test.ts +538 -0
- package/helpers/tests/testStandaloneBuild.ts +44 -0
- package/helpers/useMarkupVariables.ts +31 -0
- package/helpers/vegaConfig.ts +0 -1
- package/helpers/ver/4.24.10.ts +2 -1
- package/helpers/ver/4.24.11.ts +2 -1
- package/helpers/ver/4.24.3.ts +2 -1
- package/helpers/ver/4.24.4.ts +2 -1
- package/helpers/ver/4.24.5.ts +2 -1
- package/helpers/ver/4.24.7.ts +2 -1
- package/helpers/ver/4.24.9.ts +2 -1
- package/helpers/ver/4.25.1.ts +2 -1
- package/helpers/ver/4.25.10.ts +36 -0
- package/helpers/ver/4.25.3.ts +2 -1
- package/helpers/ver/4.25.4.ts +2 -1
- package/helpers/ver/4.25.6.ts +2 -1
- package/helpers/ver/4.25.7.ts +2 -1
- package/helpers/ver/4.25.8.ts +62 -0
- package/helpers/ver/4.25.9.ts +293 -0
- package/helpers/ver/tests/4.25.10.test.ts +204 -0
- package/helpers/ver/tests/4.25.8.test.ts +86 -0
- package/helpers/ver/tests/4.25.9.test.ts +51 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useColorPalette.ts +79 -0
- package/package.json +12 -4
- package/styles/_button-section.scss +0 -2
- package/styles/_global.scss +7 -5
- package/styles/base.scss +8 -5
- package/styles/v2/components/button.scss +4 -3
- package/styles/v2/components/editor.scss +2 -1
- package/styles/v2/layout/_data-table.scss +3 -2
- package/styles/v2/themes/_color-definitions.scss +18 -17
- package/testBuild.js +0 -0
- package/testing-setup.js +32 -0
- package/types/ForecastingSeriesKey.ts +0 -1
- package/types/MarkupInclude.ts +6 -1
- package/types/MarkupVariable.ts +19 -0
- package/types/Series.ts +4 -0
- package/types/Table.ts +1 -0
- package/types/VizFilter.ts +1 -0
- package/vitest.config.ts +16 -0
- package/components/ui/_stories/Colors.stories.mdx +0 -220
- package/components/ui/_stories/IconGallery.stories.mdx +0 -14
- package/data/colorPalettes.js +0 -171
- package/helpers/events.js +0 -14
- package/helpers/formatConfigBeforeSave.ts +0 -135
- package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
|
@@ -216,7 +216,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
216
216
|
section='table'
|
|
217
217
|
updateField={updateField}
|
|
218
218
|
/>
|
|
219
|
-
{config.table.collapsible
|
|
219
|
+
{config.table.collapsible && (
|
|
220
220
|
<CheckBox
|
|
221
221
|
value={config.table.expanded}
|
|
222
222
|
fieldName='expanded'
|
|
@@ -225,6 +225,15 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
|
|
|
225
225
|
updateField={updateField}
|
|
226
226
|
/>
|
|
227
227
|
)}
|
|
228
|
+
{config.table.collapsible && (
|
|
229
|
+
<CheckBox
|
|
230
|
+
value={config.table.showBottomCollapse}
|
|
231
|
+
fieldName='showBottomCollapse'
|
|
232
|
+
label='Show collapse below table'
|
|
233
|
+
section='table'
|
|
234
|
+
updateField={updateField}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
228
237
|
<CheckBox
|
|
229
238
|
value={config.table.download}
|
|
230
239
|
fieldName='download'
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import React, { memo, useEffect, useState, useRef, useMemo } from 'react'
|
|
2
|
+
import { useDebounce } from 'use-debounce'
|
|
3
|
+
import { MarkupVariable } from '../../../types/MarkupVariable'
|
|
4
|
+
|
|
5
|
+
type MarkupHighlightedTextFieldProps = {
|
|
6
|
+
className?: string
|
|
7
|
+
value: string | number
|
|
8
|
+
type?: 'text' | 'textarea'
|
|
9
|
+
label: string
|
|
10
|
+
placeholder?: string
|
|
11
|
+
fieldName?: string
|
|
12
|
+
section?: any
|
|
13
|
+
subsection?: any
|
|
14
|
+
updateField?: (section: any, subsection: any, fieldName: string, value: string) => void
|
|
15
|
+
markupVariables?: MarkupVariable[]
|
|
16
|
+
isEditor?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MarkupHighlightedTextField: React.FC<MarkupHighlightedTextFieldProps> = memo((props) => {
|
|
20
|
+
const {
|
|
21
|
+
value: stateValue,
|
|
22
|
+
type = 'text',
|
|
23
|
+
label,
|
|
24
|
+
placeholder = '',
|
|
25
|
+
fieldName = '',
|
|
26
|
+
section = null,
|
|
27
|
+
subsection = null,
|
|
28
|
+
updateField,
|
|
29
|
+
markupVariables = [],
|
|
30
|
+
isEditor = false,
|
|
31
|
+
className = '',
|
|
32
|
+
...attributes
|
|
33
|
+
} = props
|
|
34
|
+
|
|
35
|
+
const [value, setValue] = useState(String(stateValue))
|
|
36
|
+
const [debouncedValue] = useDebounce(value, 300) // Reduced debounce for faster response
|
|
37
|
+
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
|
|
38
|
+
const highlightRef = useRef<HTMLDivElement>(null)
|
|
39
|
+
|
|
40
|
+
// Update field when debounced value changes
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (stateValue !== debouncedValue && updateField) {
|
|
43
|
+
updateField(section, subsection, fieldName, debouncedValue)
|
|
44
|
+
}
|
|
45
|
+
}, [debouncedValue, section, subsection, fieldName, updateField, stateValue])
|
|
46
|
+
|
|
47
|
+
// Update local state when props change
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setValue(String(stateValue))
|
|
50
|
+
}, [stateValue])
|
|
51
|
+
|
|
52
|
+
// Get valid markup variable tags for highlighting (memoized)
|
|
53
|
+
const validTags = useMemo(() =>
|
|
54
|
+
markupVariables.map(variable => variable.tag).filter(Boolean),
|
|
55
|
+
[markupVariables]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Memoized highlighting function to reduce recalculations
|
|
59
|
+
const highlightedContent = useMemo(() => {
|
|
60
|
+
if (!isEditor || validTags.length === 0) {
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Escape HTML
|
|
65
|
+
const escapedText = value.replace(/[&<>"']/g, (match) => {
|
|
66
|
+
const escapeMap: { [key: string]: string } = {
|
|
67
|
+
'&': '&',
|
|
68
|
+
'<': '<',
|
|
69
|
+
'>': '>',
|
|
70
|
+
'"': '"',
|
|
71
|
+
"'": '''
|
|
72
|
+
}
|
|
73
|
+
return escapeMap[match]
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Highlight valid markup variables
|
|
77
|
+
return escapedText.replace(/\{\{([^}]+)\}\}/g, (match) => {
|
|
78
|
+
const isValid = validTags.includes(match)
|
|
79
|
+
const cssClass = isValid ? 'markup-variable-valid' : 'markup-variable-invalid'
|
|
80
|
+
return `<span class="${cssClass}">${match}</span>`
|
|
81
|
+
})
|
|
82
|
+
}, [value, validTags, isEditor])
|
|
83
|
+
|
|
84
|
+
// Optimized scroll sync
|
|
85
|
+
const handleScroll = useMemo(() => {
|
|
86
|
+
let rafId: number | null = null
|
|
87
|
+
return () => {
|
|
88
|
+
if (rafId) cancelAnimationFrame(rafId)
|
|
89
|
+
rafId = requestAnimationFrame(() => {
|
|
90
|
+
if (inputRef.current && highlightRef.current) {
|
|
91
|
+
highlightRef.current.scrollTop = inputRef.current.scrollTop
|
|
92
|
+
highlightRef.current.scrollLeft = inputRef.current.scrollLeft
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}, [])
|
|
97
|
+
|
|
98
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
99
|
+
setValue(e.target.value)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const inputId = `${fieldName}-${section}-${subsection}`
|
|
103
|
+
const shouldShowHighlighting = isEditor && validTags.length > 0
|
|
104
|
+
|
|
105
|
+
// Optimized styles
|
|
106
|
+
const containerStyle = useMemo(() => ({
|
|
107
|
+
position: 'relative' as const,
|
|
108
|
+
display: 'block',
|
|
109
|
+
width: '100%',
|
|
110
|
+
}), [])
|
|
111
|
+
|
|
112
|
+
const inputStyle = useMemo(() => ({
|
|
113
|
+
width: '100%',
|
|
114
|
+
padding: '8px 12px',
|
|
115
|
+
border: '1px solid #ddd',
|
|
116
|
+
borderRadius: '4px',
|
|
117
|
+
fontFamily: 'inherit',
|
|
118
|
+
fontSize: '14px',
|
|
119
|
+
lineHeight: '1.4',
|
|
120
|
+
background: shouldShowHighlighting ? 'transparent' : '#fff',
|
|
121
|
+
color: shouldShowHighlighting ? 'transparent' : 'inherit',
|
|
122
|
+
caretColor: shouldShowHighlighting ? '#333' : 'inherit',
|
|
123
|
+
position: 'relative' as const,
|
|
124
|
+
zIndex: 2,
|
|
125
|
+
resize: type === 'textarea' ? 'vertical' as const : 'none' as const,
|
|
126
|
+
}), [shouldShowHighlighting, type])
|
|
127
|
+
|
|
128
|
+
const highlightStyle = useMemo(() => ({
|
|
129
|
+
position: 'absolute' as const,
|
|
130
|
+
top: 0,
|
|
131
|
+
left: 0,
|
|
132
|
+
right: 0,
|
|
133
|
+
bottom: 0,
|
|
134
|
+
padding: '8px 12px',
|
|
135
|
+
margin: 0,
|
|
136
|
+
border: '1px solid transparent',
|
|
137
|
+
background: '#fff',
|
|
138
|
+
borderRadius: '4px',
|
|
139
|
+
color: '#333',
|
|
140
|
+
whiteSpace: type === 'textarea' ? 'pre-wrap' as const : 'pre' as const,
|
|
141
|
+
overflow: 'hidden',
|
|
142
|
+
pointerEvents: 'none' as const,
|
|
143
|
+
fontFamily: 'inherit',
|
|
144
|
+
fontSize: '14px',
|
|
145
|
+
lineHeight: '1.4',
|
|
146
|
+
zIndex: 1,
|
|
147
|
+
wordWrap: 'break-word' as const,
|
|
148
|
+
}), [type])
|
|
149
|
+
|
|
150
|
+
const focusStyle = useMemo(() => ({
|
|
151
|
+
outline: 'none',
|
|
152
|
+
borderColor: '#0066cc',
|
|
153
|
+
boxShadow: '0 0 0 2px rgba(0, 102, 204, 0.2)',
|
|
154
|
+
}), [])
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className={`form-group ${className}`}>
|
|
158
|
+
<label htmlFor={inputId}>
|
|
159
|
+
<span className='edit-label'>{label}</span>
|
|
160
|
+
<div style={containerStyle}>
|
|
161
|
+
{/* Highlighting overlay */}
|
|
162
|
+
{shouldShowHighlighting && (
|
|
163
|
+
<div
|
|
164
|
+
ref={highlightRef}
|
|
165
|
+
style={highlightStyle}
|
|
166
|
+
dangerouslySetInnerHTML={{ __html: highlightedContent }}
|
|
167
|
+
className="markup-highlight-overlay"
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Input field */}
|
|
172
|
+
{type === 'textarea' ? (
|
|
173
|
+
<textarea
|
|
174
|
+
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
|
|
175
|
+
id={inputId}
|
|
176
|
+
value={value}
|
|
177
|
+
onChange={handleChange}
|
|
178
|
+
onScroll={handleScroll}
|
|
179
|
+
placeholder={placeholder}
|
|
180
|
+
style={inputStyle}
|
|
181
|
+
onFocus={(e) => Object.assign(e.target.style, focusStyle)}
|
|
182
|
+
onBlur={(e) => Object.assign(e.target.style, inputStyle)}
|
|
183
|
+
{...attributes}
|
|
184
|
+
/>
|
|
185
|
+
) : (
|
|
186
|
+
<input
|
|
187
|
+
ref={inputRef as React.RefObject<HTMLInputElement>}
|
|
188
|
+
type="text"
|
|
189
|
+
id={inputId}
|
|
190
|
+
value={value}
|
|
191
|
+
onChange={handleChange}
|
|
192
|
+
onScroll={handleScroll}
|
|
193
|
+
placeholder={placeholder}
|
|
194
|
+
style={inputStyle}
|
|
195
|
+
onFocus={(e) => Object.assign(e.target.style, focusStyle)}
|
|
196
|
+
onBlur={(e) => Object.assign(e.target.style, inputStyle)}
|
|
197
|
+
{...attributes}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
</label>
|
|
202
|
+
|
|
203
|
+
<style jsx>{`
|
|
204
|
+
.markup-highlight-overlay .markup-variable-valid {
|
|
205
|
+
background-color: #e6f3ff;
|
|
206
|
+
color: #0066cc;
|
|
207
|
+
border-radius: 3px;
|
|
208
|
+
padding: 0px 2px;
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.markup-highlight-overlay .markup-variable-invalid {
|
|
213
|
+
background-color: #ffe6e6;
|
|
214
|
+
color: #cc0000;
|
|
215
|
+
border-radius: 3px;
|
|
216
|
+
padding: 0px 2px;
|
|
217
|
+
font-weight: 500;
|
|
218
|
+
text-decoration: underline wavy #cc0000;
|
|
219
|
+
}
|
|
220
|
+
`}</style>
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
MarkupHighlightedTextField.displayName = 'MarkupHighlightedTextField'
|
|
226
|
+
|
|
227
|
+
export default MarkupHighlightedTextField
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react'
|
|
2
|
+
import { MarkupVariable, MarkupCondition } from '../../../types/MarkupVariable'
|
|
3
|
+
import Button from '../../elements/Button'
|
|
4
|
+
import { TextField, Select, CheckBox } from '../Inputs'
|
|
5
|
+
import Icon from '../../ui/Icon'
|
|
6
|
+
import Accordion from '../../ui/Accordion'
|
|
7
|
+
|
|
8
|
+
type MarkupVariablesEditorProps = {
|
|
9
|
+
/** Array of markup variable configurations */
|
|
10
|
+
markupVariables: MarkupVariable[]
|
|
11
|
+
/** Dataset to extract column names and values from */
|
|
12
|
+
data: any[]
|
|
13
|
+
/** Callback when variables are added, updated, or removed */
|
|
14
|
+
onChange: (variables: MarkupVariable[]) => void
|
|
15
|
+
/** Whether markup variables feature is enabled */
|
|
16
|
+
enableMarkupVariables?: boolean
|
|
17
|
+
/** Callback when enable/disable toggle changes */
|
|
18
|
+
onToggleEnable?: (enabled: boolean) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type { MarkupVariablesEditorProps }
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Editor for creating and managing markup variables with {{variable-name}} syntax.
|
|
25
|
+
* Supports conditional filters, number formatting, and auto-generated tags.
|
|
26
|
+
*/
|
|
27
|
+
const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
|
|
28
|
+
markupVariables = [],
|
|
29
|
+
data = [],
|
|
30
|
+
onChange,
|
|
31
|
+
enableMarkupVariables = false,
|
|
32
|
+
onToggleEnable
|
|
33
|
+
}) => {
|
|
34
|
+
const [editingIndex, setEditingIndex] = useState<number | null>(null)
|
|
35
|
+
const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({})
|
|
36
|
+
|
|
37
|
+
// Ensure we always have a valid array
|
|
38
|
+
const safeMarkupVariables = markupVariables || []
|
|
39
|
+
const availableColumns = data.length > 0 ? Object.keys(data[0]) : []
|
|
40
|
+
|
|
41
|
+
// Validate a variable and return array of error messages
|
|
42
|
+
const validateVariable = React.useCallback((variable: MarkupVariable): string[] => {
|
|
43
|
+
const errors: string[] = []
|
|
44
|
+
if (!variable.name || variable.name.trim() === '') {
|
|
45
|
+
errors.push('Variable name is required')
|
|
46
|
+
}
|
|
47
|
+
if (!variable.tag || variable.tag.trim() === '') {
|
|
48
|
+
errors.push('Variable tag is required')
|
|
49
|
+
}
|
|
50
|
+
if (!variable.columnName || variable.columnName.trim() === '') {
|
|
51
|
+
errors.push('Data column is required')
|
|
52
|
+
}
|
|
53
|
+
// Validate conditions
|
|
54
|
+
variable.conditions?.forEach((condition, index) => {
|
|
55
|
+
if (!condition.columnName) {
|
|
56
|
+
errors.push(`Condition ${index + 1}: Column is required`)
|
|
57
|
+
}
|
|
58
|
+
if (!condition.value) {
|
|
59
|
+
errors.push(`Condition ${index + 1}: Value is required`)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
return errors
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
// Validate all variables on mount and when variables change
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
const errors: Record<number, string[]> = {}
|
|
68
|
+
safeMarkupVariables.forEach((variable, index) => {
|
|
69
|
+
const variableErrors = validateVariable(variable)
|
|
70
|
+
if (variableErrors.length > 0) {
|
|
71
|
+
errors[index] = variableErrors
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
setValidationErrors(errors)
|
|
75
|
+
}, [safeMarkupVariables, validateVariable]) // Re-validate when variables change
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
const addVariable = () => {
|
|
79
|
+
const newVariable: MarkupVariable = {
|
|
80
|
+
name: '',
|
|
81
|
+
tag: '',
|
|
82
|
+
columnName: '',
|
|
83
|
+
conditions: [],
|
|
84
|
+
addCommas: false,
|
|
85
|
+
hideOnNull: false
|
|
86
|
+
}
|
|
87
|
+
const newVariables = [...safeMarkupVariables, newVariable]
|
|
88
|
+
onChange(newVariables)
|
|
89
|
+
const newIndex = safeMarkupVariables.length
|
|
90
|
+
setEditingIndex(newIndex)
|
|
91
|
+
|
|
92
|
+
// Immediately show validation errors for the new empty variable
|
|
93
|
+
const errors = validateVariable(newVariable)
|
|
94
|
+
setValidationErrors(prev => ({
|
|
95
|
+
...prev,
|
|
96
|
+
[newIndex]: errors
|
|
97
|
+
}))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const updateVariable = (index: number, updates: Partial<MarkupVariable>) => {
|
|
101
|
+
const updated = safeMarkupVariables.map((variable, i) =>
|
|
102
|
+
i === index ? { ...variable, conditions: variable.conditions || [], ...updates } : variable
|
|
103
|
+
)
|
|
104
|
+
onChange(updated)
|
|
105
|
+
|
|
106
|
+
// Validate and update errors for this variable
|
|
107
|
+
const errors = validateVariable(updated[index])
|
|
108
|
+
setValidationErrors(prev => ({
|
|
109
|
+
...prev,
|
|
110
|
+
[index]: errors
|
|
111
|
+
}))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const removeVariable = (index: number) => {
|
|
115
|
+
const updated = safeMarkupVariables.filter((_, i) => i !== index)
|
|
116
|
+
onChange(updated)
|
|
117
|
+
if (editingIndex === index) {
|
|
118
|
+
setEditingIndex(null)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const generateTag = (name: string) => {
|
|
123
|
+
if (!name) return ''
|
|
124
|
+
// Convert name to tag format: "My Variable" -> "{{my-variable}}"
|
|
125
|
+
return `{{${name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')}}}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get unique values for a given column for condition dropdowns
|
|
129
|
+
const getColumnValues = useMemo(() => {
|
|
130
|
+
if (!data || data.length === 0) return {}
|
|
131
|
+
|
|
132
|
+
const columnValues: Record<string, (string | number)[]> = {}
|
|
133
|
+
availableColumns.forEach(column => {
|
|
134
|
+
const uniqueValues = Array.from(new Set(data.map(row => row[column])))
|
|
135
|
+
.filter(val => val !== null && val !== undefined && val !== '')
|
|
136
|
+
columnValues[column] = uniqueValues
|
|
137
|
+
})
|
|
138
|
+
return columnValues
|
|
139
|
+
}, [data, availableColumns])
|
|
140
|
+
|
|
141
|
+
const addCondition = (variableIndex: number) => {
|
|
142
|
+
const variable = safeMarkupVariables[variableIndex]
|
|
143
|
+
const newCondition: MarkupCondition = {
|
|
144
|
+
columnName: '',
|
|
145
|
+
isOrIsNotEqualTo: 'is',
|
|
146
|
+
value: ''
|
|
147
|
+
}
|
|
148
|
+
const updatedConditions = [...(variable.conditions || []), newCondition]
|
|
149
|
+
updateVariable(variableIndex, { conditions: updatedConditions })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const updateCondition = (variableIndex: number, conditionIndex: number, updates: Partial<MarkupCondition>) => {
|
|
153
|
+
const variable = safeMarkupVariables[variableIndex]
|
|
154
|
+
const conditions = variable.conditions || []
|
|
155
|
+
const updatedConditions = conditions.map((condition, i) =>
|
|
156
|
+
i === conditionIndex ? { ...condition, ...updates } : condition
|
|
157
|
+
)
|
|
158
|
+
updateVariable(variableIndex, { conditions: updatedConditions })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const removeCondition = (variableIndex: number, conditionIndex: number) => {
|
|
162
|
+
const variable = safeMarkupVariables[variableIndex]
|
|
163
|
+
const updatedConditions = variable.conditions.filter((_, i) => i !== conditionIndex)
|
|
164
|
+
updateVariable(variableIndex, { conditions: updatedConditions })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className='markup-variables-editor'>
|
|
169
|
+
<div className='mb-3'>
|
|
170
|
+
<CheckBox
|
|
171
|
+
value={enableMarkupVariables}
|
|
172
|
+
fieldName='enableMarkupVariables'
|
|
173
|
+
label='Enable Markup Variables'
|
|
174
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
175
|
+
if (onToggleEnable) {
|
|
176
|
+
onToggleEnable(value)
|
|
177
|
+
}
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{enableMarkupVariables && (
|
|
183
|
+
<>
|
|
184
|
+
<div className='mb-3'>
|
|
185
|
+
<p className='text-sm text-gray-600'>
|
|
186
|
+
Use variables in your content with <code>{'{{variable-name}}'}</code> syntax.
|
|
187
|
+
Variables will be replaced with values from your data.
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{safeMarkupVariables.length > 0 && (
|
|
192
|
+
<div className='variables-list mb-3'>
|
|
193
|
+
{safeMarkupVariables.map((variable, index) => variable ? (
|
|
194
|
+
<div key={index} className='variable-item p-3 border rounded mb-2' style={{ backgroundColor: '#fff' }}>
|
|
195
|
+
<div className='d-flex justify-content-between align-items-start mb-2'>
|
|
196
|
+
<div style={{ flex: 1 }}>
|
|
197
|
+
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '4px' }}>
|
|
198
|
+
{variable.name || 'Unnamed Variable'}
|
|
199
|
+
</div>
|
|
200
|
+
<div style={{ fontSize: '13px', color: '#6c757d', fontFamily: 'monospace', marginBottom: '4px' }}>
|
|
201
|
+
{variable.tag}
|
|
202
|
+
</div>
|
|
203
|
+
<div style={{ fontSize: '13px', color: '#6c757d' }}>
|
|
204
|
+
Column: <strong>{variable.columnName || 'Not selected'}</strong>
|
|
205
|
+
{variable.conditions && variable.conditions.length > 0 && (
|
|
206
|
+
<span> • {variable.conditions.length} condition{variable.conditions.length !== 1 ? 's' : ''}</span>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
{validationErrors[index] && validationErrors[index].length > 0 && (
|
|
210
|
+
<div style={{ fontSize: '12px', color: '#dc3545', marginTop: '8px' }}>
|
|
211
|
+
<strong>⚠ Validation Errors:</strong>
|
|
212
|
+
<ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
|
|
213
|
+
{validationErrors[index].map((error, errorIndex) => (
|
|
214
|
+
<li key={errorIndex}>{error}</li>
|
|
215
|
+
))}
|
|
216
|
+
</ul>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
<div className='d-flex gap-2'>
|
|
221
|
+
<Button
|
|
222
|
+
className='btn-sm'
|
|
223
|
+
onClick={() => setEditingIndex(editingIndex === index ? null : index)}
|
|
224
|
+
>
|
|
225
|
+
{editingIndex === index ? 'Close' : 'Edit'}
|
|
226
|
+
</Button>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{editingIndex === index && (
|
|
231
|
+
<div className='mt-3 pt-3 border-t'>
|
|
232
|
+
<Accordion>
|
|
233
|
+
<Accordion.Section title='Basic Settings'>
|
|
234
|
+
<div className='mb-3'>
|
|
235
|
+
<TextField
|
|
236
|
+
value={variable.name}
|
|
237
|
+
fieldName='name'
|
|
238
|
+
label='Variable Name'
|
|
239
|
+
placeholder='e.g., "State Name"'
|
|
240
|
+
updateField={(section, subsection, fieldName, value) => {
|
|
241
|
+
updateVariable(index, {
|
|
242
|
+
name: value,
|
|
243
|
+
tag: generateTag(value)
|
|
244
|
+
})
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
<div className='mb-3'>
|
|
249
|
+
<label>
|
|
250
|
+
<span className='edit-label column-heading'>Tag (auto-generated)</span>
|
|
251
|
+
<input
|
|
252
|
+
type='text'
|
|
253
|
+
value={variable.tag}
|
|
254
|
+
placeholder='{{variable-name}}'
|
|
255
|
+
readOnly
|
|
256
|
+
style={{ backgroundColor: '#e9ecef', cursor: 'not-allowed' }}
|
|
257
|
+
/>
|
|
258
|
+
</label>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div className='mb-3'>
|
|
262
|
+
<Select
|
|
263
|
+
value={variable.columnName}
|
|
264
|
+
fieldName='columnName'
|
|
265
|
+
label='Data Column'
|
|
266
|
+
options={[
|
|
267
|
+
{ value: '', label: 'Select Column...' },
|
|
268
|
+
...availableColumns.map(col => ({ value: col, label: col }))
|
|
269
|
+
]}
|
|
270
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
271
|
+
updateVariable(index, { columnName: value })
|
|
272
|
+
}}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
</Accordion.Section>
|
|
276
|
+
|
|
277
|
+
<Accordion.Section title='Conditions'>
|
|
278
|
+
<div className='text-sm text-gray-500 mb-2'>
|
|
279
|
+
Add conditions to filter when this variable should display data
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{variable.conditions && variable.conditions.length > 0 && (
|
|
283
|
+
<div className='conditions-list mb-2'>
|
|
284
|
+
{variable.conditions.map((condition, conditionIndex) => (
|
|
285
|
+
<div key={`condition-${index}-${conditionIndex}`} className='condition-item p-2 border rounded mb-2' style={{ backgroundColor: '#f8f9fa' }}>
|
|
286
|
+
<div className='mb-2'>
|
|
287
|
+
<Select
|
|
288
|
+
value={condition.columnName || ''}
|
|
289
|
+
fieldName={`condition-column-${index}-${conditionIndex}`}
|
|
290
|
+
label='Column'
|
|
291
|
+
options={[
|
|
292
|
+
{ value: '', label: 'Select Column...' },
|
|
293
|
+
...availableColumns.map(col => ({ value: col, label: col }))
|
|
294
|
+
]}
|
|
295
|
+
updateField={(_section, _subsection, _fieldName, newColumnName) => {
|
|
296
|
+
// Reset value when column changes
|
|
297
|
+
updateCondition(index, conditionIndex, {
|
|
298
|
+
columnName: newColumnName,
|
|
299
|
+
value: ''
|
|
300
|
+
})
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
<div className='mb-2'>
|
|
305
|
+
<Select
|
|
306
|
+
value={condition.isOrIsNotEqualTo || 'is'}
|
|
307
|
+
fieldName={`condition-operator-${index}-${conditionIndex}`}
|
|
308
|
+
label='Operator'
|
|
309
|
+
options={[
|
|
310
|
+
{ value: 'is', label: 'is' },
|
|
311
|
+
{ value: 'is not', label: 'is not' }
|
|
312
|
+
]}
|
|
313
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
314
|
+
updateCondition(index, conditionIndex, { isOrIsNotEqualTo: value as 'is' | 'is not' })
|
|
315
|
+
}}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
<div className='mb-2'>
|
|
319
|
+
<Select
|
|
320
|
+
value={condition.value || ''}
|
|
321
|
+
fieldName={`condition-value-${index}-${conditionIndex}`}
|
|
322
|
+
label='Value'
|
|
323
|
+
options={[
|
|
324
|
+
{ value: '', label: 'Select Value...' },
|
|
325
|
+
...(condition.columnName && getColumnValues[condition.columnName]
|
|
326
|
+
? getColumnValues[condition.columnName].map(val => ({
|
|
327
|
+
value: String(val),
|
|
328
|
+
label: String(val)
|
|
329
|
+
}))
|
|
330
|
+
: [])
|
|
331
|
+
]}
|
|
332
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
333
|
+
updateCondition(index, conditionIndex, { value })
|
|
334
|
+
}}
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
<Button
|
|
338
|
+
className='btn-sm btn-danger'
|
|
339
|
+
onClick={() => removeCondition(index, conditionIndex)}
|
|
340
|
+
>
|
|
341
|
+
Remove Condition
|
|
342
|
+
</Button>
|
|
343
|
+
</div>
|
|
344
|
+
))}
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
|
|
348
|
+
<Button
|
|
349
|
+
className='btn-sm'
|
|
350
|
+
onClick={() => addCondition(index)}
|
|
351
|
+
>
|
|
352
|
+
<Icon display='plus' size={14} className='mr-1' />
|
|
353
|
+
Add Condition
|
|
354
|
+
</Button>
|
|
355
|
+
</Accordion.Section>
|
|
356
|
+
|
|
357
|
+
<Accordion.Section title='Formatting Options'>
|
|
358
|
+
<div className='mb-3'>
|
|
359
|
+
<CheckBox
|
|
360
|
+
value={variable.addCommas || false}
|
|
361
|
+
fieldName='addCommas'
|
|
362
|
+
label='Format numbers with commas'
|
|
363
|
+
updateField={(_section, _subsection, _fieldName, value) =>
|
|
364
|
+
updateVariable(index, { addCommas: value })
|
|
365
|
+
}
|
|
366
|
+
/>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<div className='mb-3'>
|
|
370
|
+
<CheckBox
|
|
371
|
+
value={variable.hideOnNull || false}
|
|
372
|
+
fieldName='hideOnNull'
|
|
373
|
+
label='Hide section when value is null'
|
|
374
|
+
updateField={(_section, _subsection, _fieldName, value) =>
|
|
375
|
+
updateVariable(index, { hideOnNull: value })
|
|
376
|
+
}
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
</Accordion.Section>
|
|
380
|
+
</Accordion>
|
|
381
|
+
|
|
382
|
+
<div className='mt-3 pt-3 border-t' style={{ textAlign: 'center' }}>
|
|
383
|
+
<Button
|
|
384
|
+
className='btn-sm btn-danger'
|
|
385
|
+
onClick={() => {
|
|
386
|
+
if (window.confirm(`Are you sure you want to delete the variable "${variable.name || 'Unnamed Variable'}"?`)) {
|
|
387
|
+
removeVariable(index)
|
|
388
|
+
}
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
Delete Variable
|
|
392
|
+
</Button>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
) : <></>)}
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
<Button className='btn-primary' onClick={addVariable}>
|
|
402
|
+
<Icon display='plus' size={16} className='mr-2' />
|
|
403
|
+
Add Variable
|
|
404
|
+
</Button>
|
|
405
|
+
</>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export default MarkupVariablesEditor
|