@cdc/core 4.25.8 → 4.25.11
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/_stories/StoryRenderingTests.stories.tsx +164 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +32 -9
- package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
- package/components/CustomColorsEditor/index.ts +1 -0
- package/components/DataTable/DataTable.tsx +56 -38
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/ChartHeader.tsx +44 -14
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/components/ExpandCollapse.tsx +10 -1
- package/components/DataTable/components/MapHeader.tsx +24 -13
- package/components/DataTable/data-table.css +12 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
- package/components/DataTable/helpers/mapCellMatrix.tsx +33 -4
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- package/components/DownloadButton.tsx +40 -14
- package/components/EditorPanel/DataTableEditor.tsx +3 -3
- package/components/EditorPanel/EditorPanel.styles.css +423 -0
- package/components/EditorPanel/FootnotesEditor.tsx +44 -37
- package/components/EditorPanel/Inputs.tsx +12 -2
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -2
- package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +450 -0
- package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
- package/components/ErrorBoundary.jsx +3 -1
- package/components/Filters/Filters.tsx +52 -24
- package/components/Filters/components/Dropdown.tsx +6 -1
- package/components/Filters/components/Tabs.tsx +1 -0
- package/components/Footnotes/Footnotes.tsx +35 -25
- package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
- package/components/HeaderThemeSelector/index.ts +2 -0
- package/components/Layout/styles/editor.scss +2 -1
- package/components/Legend/Legend.Gradient.tsx +3 -6
- package/components/LegendShape.tsx +121 -3
- package/components/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +72 -21
- package/components/PaletteConversionModal.tsx +90 -0
- package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
- package/components/PaletteSelector/PaletteSelector.css +94 -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/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
- package/components/_stories/DataTable.stories.tsx +1 -1
- package/components/_stories/Filters.stories.tsx +21 -2
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +769 -4
- package/components/_stories/Inputs.stories.tsx +3 -3
- 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/_stories/styles.scss +0 -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/Accordion.jsx +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/components/ui/accordion.styles.css +57 -0
- 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 +63 -14
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +80 -0
- package/helpers/addValuesToFilters.ts +7 -3
- package/helpers/cloneConfig.ts +31 -0
- package/helpers/configDataHelpers.ts +128 -0
- package/helpers/configHelpers.ts +27 -0
- package/helpers/constants.ts +42 -2
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +15 -3
- package/helpers/fetchRemoteData.ts +3 -15
- 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 +220 -0
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/metrics/helpers.ts +42 -19
- package/helpers/metrics/types.ts +48 -9
- 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/parseCsvWithQuotes.ts +65 -0
- package/helpers/queryStringUtils.ts +13 -0
- package/helpers/testing.ts +358 -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.11.ts +13 -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 +2 -1
- 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 +1 -1
- 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 +13 -4
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +32 -10
- package/styles/base.scss +8 -55
- package/styles/cove-main.scss +3 -1
- package/styles/filters.scss +10 -3
- package/styles/v2/base/index.scss +0 -1
- package/styles/v2/components/button.scss +4 -3
- package/styles/v2/components/editor.scss +16 -7
- package/styles/v2/layout/_data-table.scss +3 -2
- package/styles/v2/themes/_color-definitions.scss +18 -17
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/testing-setup.js +32 -0
- package/types/MarkupInclude.ts +8 -2
- package/types/MarkupVariable.ts +19 -0
- package/types/VizFilter.ts +2 -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/formatConfigBeforeSave.ts +0 -135
- package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
- package/styles/_mixins.scss +0 -13
- package/styles/v2/base/_typography.scss +0 -0
- package/styles/v2/components/guidance-block.scss +0 -74
- package/styles/v2/utils/_functions.scss +0 -0
- /package/{styles/_typography.scss → testBuild.js} +0 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } 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
|
+
import { Datasets } from '../../../types/DataSet'
|
|
8
|
+
import _ from 'lodash'
|
|
9
|
+
|
|
10
|
+
type MarkupVariablesEditorProps = {
|
|
11
|
+
/** Array of markup variable configurations */
|
|
12
|
+
markupVariables: MarkupVariable[]
|
|
13
|
+
/** Dataset to extract column names and values from (for backward compatibility) */
|
|
14
|
+
data?: any[]
|
|
15
|
+
/** Available datasets for multi-dataset support */
|
|
16
|
+
datasets?: Datasets
|
|
17
|
+
/** Configuration object containing dataKey for dataset assignment */
|
|
18
|
+
config?: { dataKey?: string }
|
|
19
|
+
/** Callback when variables are added, updated, or removed */
|
|
20
|
+
onChange: (variables: MarkupVariable[]) => void
|
|
21
|
+
/** Whether markup variables feature is enabled */
|
|
22
|
+
enableMarkupVariables?: boolean
|
|
23
|
+
/** Callback when enable/disable toggle changes */
|
|
24
|
+
onToggleEnable?: (enabled: boolean) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type { MarkupVariablesEditorProps }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Editor for creating and managing markup variables with {{variable-name}} syntax.
|
|
31
|
+
* Supports conditional filters, number formatting, and auto-generated tags.
|
|
32
|
+
*/
|
|
33
|
+
const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
|
|
34
|
+
markupVariables = [],
|
|
35
|
+
data = [],
|
|
36
|
+
datasets,
|
|
37
|
+
config,
|
|
38
|
+
onChange,
|
|
39
|
+
enableMarkupVariables = false,
|
|
40
|
+
onToggleEnable
|
|
41
|
+
}) => {
|
|
42
|
+
const [editingIndex, setEditingIndex] = useState<number | null>(null)
|
|
43
|
+
const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({})
|
|
44
|
+
|
|
45
|
+
// Ensure we always have a valid array (memoized with deep equality to prevent unnecessary re-renders)
|
|
46
|
+
const safeMarkupVariables = useMemo(() => markupVariables || [], [JSON.stringify(markupVariables)])
|
|
47
|
+
|
|
48
|
+
// Get the target dataset with fallback logic (memoized for performance)
|
|
49
|
+
const getTargetData = useCallback((): any[] => {
|
|
50
|
+
// First try to use the data prop
|
|
51
|
+
if (data && data.length > 0) {
|
|
52
|
+
return data
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback to assigned dataset using config.dataKey
|
|
56
|
+
if (datasets && config?.dataKey) {
|
|
57
|
+
const assignedDataset = datasets[config.dataKey]
|
|
58
|
+
if (assignedDataset?.data?.length > 0) {
|
|
59
|
+
return assignedDataset.data
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return []
|
|
64
|
+
}, [data, datasets, config?.dataKey])
|
|
65
|
+
|
|
66
|
+
// Get columns from the available data (memoized for performance)
|
|
67
|
+
const getAvailableColumns = useMemo((): string[] => {
|
|
68
|
+
const targetData = getTargetData()
|
|
69
|
+
return targetData.length > 0 ? Object.keys(targetData[0]) : []
|
|
70
|
+
}, [getTargetData])
|
|
71
|
+
|
|
72
|
+
// Get column values for a specific column (memoized for performance)
|
|
73
|
+
const getColumnValues = useCallback((columnName: string): string[] => {
|
|
74
|
+
const targetData = getTargetData()
|
|
75
|
+
if (targetData.length === 0) return []
|
|
76
|
+
|
|
77
|
+
const uniqueValues = new Set<string>()
|
|
78
|
+
targetData.forEach(row => {
|
|
79
|
+
if (row[columnName] !== undefined && row[columnName] !== null) {
|
|
80
|
+
uniqueValues.add(String(row[columnName]))
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
return Array.from(uniqueValues).sort()
|
|
84
|
+
}, [data, datasets, config?.dataKey])
|
|
85
|
+
|
|
86
|
+
// Validate a variable and return array of error messages
|
|
87
|
+
const validateVariable = React.useCallback((variable: MarkupVariable): string[] => {
|
|
88
|
+
const errors: string[] = []
|
|
89
|
+
if (!variable.name || variable.name.trim() === '') {
|
|
90
|
+
errors.push('Variable name is required')
|
|
91
|
+
}
|
|
92
|
+
if (!variable.tag || variable.tag.trim() === '') {
|
|
93
|
+
errors.push('Variable tag is required')
|
|
94
|
+
}
|
|
95
|
+
if (!variable.columnName || variable.columnName.trim() === '') {
|
|
96
|
+
errors.push('Data column is required')
|
|
97
|
+
}
|
|
98
|
+
// Validate conditions
|
|
99
|
+
variable.conditions?.forEach((condition, index) => {
|
|
100
|
+
if (!condition.columnName) {
|
|
101
|
+
errors.push(`Condition ${index + 1}: Column is required`)
|
|
102
|
+
}
|
|
103
|
+
if (!condition.value) {
|
|
104
|
+
errors.push(`Condition ${index + 1}: Value is required`)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
return errors
|
|
108
|
+
}, [])
|
|
109
|
+
|
|
110
|
+
// Validate all variables on mount and when variables change
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
const errors: Record<number, string[]> = {}
|
|
113
|
+
safeMarkupVariables.forEach((variable, index) => {
|
|
114
|
+
const variableErrors = validateVariable(variable)
|
|
115
|
+
if (variableErrors.length > 0) {
|
|
116
|
+
errors[index] = variableErrors
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Only update if errors have actually changed (use deep equality)
|
|
121
|
+
setValidationErrors(prev => {
|
|
122
|
+
const errorsChanged = !_.isEqual(prev, errors)
|
|
123
|
+
return errorsChanged ? errors : prev
|
|
124
|
+
})
|
|
125
|
+
}, [safeMarkupVariables, validateVariable]) // Re-validate when variables change
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
const addVariable = () => {
|
|
129
|
+
const newVariable: MarkupVariable = {
|
|
130
|
+
name: '',
|
|
131
|
+
tag: '',
|
|
132
|
+
columnName: '',
|
|
133
|
+
conditions: [],
|
|
134
|
+
addCommas: false,
|
|
135
|
+
hideOnNull: false
|
|
136
|
+
}
|
|
137
|
+
const newVariables = [...safeMarkupVariables, newVariable]
|
|
138
|
+
onChange(newVariables)
|
|
139
|
+
const newIndex = safeMarkupVariables.length
|
|
140
|
+
setEditingIndex(newIndex)
|
|
141
|
+
|
|
142
|
+
// Immediately show validation errors for the new empty variable
|
|
143
|
+
const errors = validateVariable(newVariable)
|
|
144
|
+
setValidationErrors(prev => ({
|
|
145
|
+
...prev,
|
|
146
|
+
[newIndex]: errors
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const updateVariable = (index: number, updates: Partial<MarkupVariable>) => {
|
|
151
|
+
const updated = safeMarkupVariables.map((variable, i) =>
|
|
152
|
+
i === index ? { ...variable, conditions: variable.conditions || [], ...updates } : variable
|
|
153
|
+
)
|
|
154
|
+
onChange(updated)
|
|
155
|
+
|
|
156
|
+
// Validate and update errors for this variable
|
|
157
|
+
const errors = validateVariable(updated[index])
|
|
158
|
+
setValidationErrors(prev => ({
|
|
159
|
+
...prev,
|
|
160
|
+
[index]: errors
|
|
161
|
+
}))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const removeVariable = (index: number) => {
|
|
165
|
+
const updated = safeMarkupVariables.filter((_, i) => i !== index)
|
|
166
|
+
onChange(updated)
|
|
167
|
+
if (editingIndex === index) {
|
|
168
|
+
setEditingIndex(null)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const generateTag = (name: string) => {
|
|
173
|
+
if (!name) return ''
|
|
174
|
+
// Convert name to tag format: "My Variable" -> "{{my-variable}}"
|
|
175
|
+
return `{{${name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')}}}`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
const addCondition = (variableIndex: number) => {
|
|
181
|
+
const variable = safeMarkupVariables[variableIndex]
|
|
182
|
+
const newCondition: MarkupCondition = {
|
|
183
|
+
columnName: '',
|
|
184
|
+
isOrIsNotEqualTo: 'is',
|
|
185
|
+
value: ''
|
|
186
|
+
}
|
|
187
|
+
const updatedConditions = [...(variable.conditions || []), newCondition]
|
|
188
|
+
updateVariable(variableIndex, { conditions: updatedConditions })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const updateCondition = (variableIndex: number, conditionIndex: number, updates: Partial<MarkupCondition>) => {
|
|
192
|
+
const variable = safeMarkupVariables[variableIndex]
|
|
193
|
+
const conditions = variable.conditions || []
|
|
194
|
+
const updatedConditions = conditions.map((condition, i) =>
|
|
195
|
+
i === conditionIndex ? { ...condition, ...updates } : condition
|
|
196
|
+
)
|
|
197
|
+
updateVariable(variableIndex, { conditions: updatedConditions })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const removeCondition = (variableIndex: number, conditionIndex: number) => {
|
|
201
|
+
const variable = safeMarkupVariables[variableIndex]
|
|
202
|
+
const updatedConditions = variable.conditions.filter((_, i) => i !== conditionIndex)
|
|
203
|
+
updateVariable(variableIndex, { conditions: updatedConditions })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className='markup-variables-editor'>
|
|
208
|
+
<div className='mb-3'>
|
|
209
|
+
<CheckBox
|
|
210
|
+
value={enableMarkupVariables}
|
|
211
|
+
fieldName='enableMarkupVariables'
|
|
212
|
+
label='Enable Markup Variables'
|
|
213
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
214
|
+
if (onToggleEnable) {
|
|
215
|
+
onToggleEnable(value)
|
|
216
|
+
}
|
|
217
|
+
}}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{enableMarkupVariables && (
|
|
222
|
+
<>
|
|
223
|
+
<div className='mb-3'>
|
|
224
|
+
<p className='text-sm text-gray-600'>
|
|
225
|
+
Use variables in your content with <code>{'{{variable-name}}'}</code> syntax.
|
|
226
|
+
Variables will be replaced with values from your data.
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{safeMarkupVariables.length > 0 && (
|
|
231
|
+
<div className='variables-list mb-3'>
|
|
232
|
+
{safeMarkupVariables.map((variable, index) => variable ? (
|
|
233
|
+
<div key={index} className='variable-item p-3 border rounded mb-2' style={{ backgroundColor: '#fff' }}>
|
|
234
|
+
<div className='d-flex justify-content-between align-items-start mb-2'>
|
|
235
|
+
<div style={{ flex: 1 }}>
|
|
236
|
+
<div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '4px' }}>
|
|
237
|
+
{variable.name || 'Unnamed Variable'}
|
|
238
|
+
</div>
|
|
239
|
+
<div style={{ fontSize: '13px', color: '#6c757d', fontFamily: 'monospace', marginBottom: '4px' }}>
|
|
240
|
+
{variable.tag}
|
|
241
|
+
</div>
|
|
242
|
+
<div style={{ fontSize: '13px', color: '#6c757d' }}>
|
|
243
|
+
Column: <strong>{variable.columnName || 'Not selected'}</strong>
|
|
244
|
+
{variable.conditions && variable.conditions.length > 0 && (
|
|
245
|
+
<span> • {variable.conditions.length} condition{variable.conditions.length !== 1 ? 's' : ''}</span>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
{validationErrors[index] && validationErrors[index].length > 0 && (
|
|
249
|
+
<div style={{ fontSize: '12px', color: '#dc3545', marginTop: '8px' }}>
|
|
250
|
+
<strong>⚠ Validation Errors:</strong>
|
|
251
|
+
<ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
|
|
252
|
+
{validationErrors[index].map((error, errorIndex) => (
|
|
253
|
+
<li key={errorIndex}>{error}</li>
|
|
254
|
+
))}
|
|
255
|
+
</ul>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
<div className='d-flex gap-2'>
|
|
260
|
+
<Button
|
|
261
|
+
className='btn-sm'
|
|
262
|
+
onClick={() => setEditingIndex(editingIndex === index ? null : index)}
|
|
263
|
+
>
|
|
264
|
+
{editingIndex === index ? 'Close' : 'Edit'}
|
|
265
|
+
</Button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{editingIndex === index && (
|
|
270
|
+
<div className='mt-3 pt-3 border-t'>
|
|
271
|
+
<Accordion>
|
|
272
|
+
<Accordion.Section title='Basic Settings'>
|
|
273
|
+
<div className='mb-3'>
|
|
274
|
+
<TextField
|
|
275
|
+
value={variable.name}
|
|
276
|
+
fieldName='name'
|
|
277
|
+
label='Variable Name'
|
|
278
|
+
placeholder='e.g., "State Name"'
|
|
279
|
+
updateField={(section, subsection, fieldName, value) => {
|
|
280
|
+
updateVariable(index, {
|
|
281
|
+
name: value,
|
|
282
|
+
tag: generateTag(value)
|
|
283
|
+
})
|
|
284
|
+
}}
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
<div className='mb-3'>
|
|
288
|
+
<label>
|
|
289
|
+
<span className='edit-label column-heading'>Tag (auto-generated)</span>
|
|
290
|
+
<input
|
|
291
|
+
type='text'
|
|
292
|
+
value={variable.tag}
|
|
293
|
+
placeholder='{{variable-name}}'
|
|
294
|
+
readOnly
|
|
295
|
+
style={{ backgroundColor: '#e9ecef', cursor: 'not-allowed' }}
|
|
296
|
+
/>
|
|
297
|
+
</label>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div className='mb-3'>
|
|
301
|
+
<Select
|
|
302
|
+
value={variable.columnName}
|
|
303
|
+
fieldName='columnName'
|
|
304
|
+
label='Data Column'
|
|
305
|
+
options={[
|
|
306
|
+
{ value: '', label: 'Select Column...' },
|
|
307
|
+
...getAvailableColumns.map(col => ({ value: col, label: col }))
|
|
308
|
+
]}
|
|
309
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
310
|
+
updateVariable(index, { columnName: value })
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
</Accordion.Section>
|
|
315
|
+
|
|
316
|
+
<Accordion.Section title='Conditions'>
|
|
317
|
+
<div className='text-sm text-gray-500 mb-2'>
|
|
318
|
+
Add conditions to filter when this variable should display data
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{variable.conditions && variable.conditions.length > 0 && (
|
|
322
|
+
<div className='conditions-list mb-2'>
|
|
323
|
+
{variable.conditions.map((condition, conditionIndex) => (
|
|
324
|
+
<div key={`condition-${index}-${conditionIndex}`} className='condition-item p-2 border rounded mb-2' style={{ backgroundColor: '#f8f9fa' }}>
|
|
325
|
+
<div className='mb-2'>
|
|
326
|
+
<Select
|
|
327
|
+
value={condition.columnName || ''}
|
|
328
|
+
fieldName={`condition-column-${index}-${conditionIndex}`}
|
|
329
|
+
label='Column'
|
|
330
|
+
options={[
|
|
331
|
+
{ value: '', label: 'Select Column...' },
|
|
332
|
+
...getAvailableColumns.map(col => ({ value: col, label: col }))
|
|
333
|
+
]}
|
|
334
|
+
updateField={(_section, _subsection, _fieldName, newColumnName) => {
|
|
335
|
+
// Reset value when column changes
|
|
336
|
+
updateCondition(index, conditionIndex, {
|
|
337
|
+
columnName: newColumnName,
|
|
338
|
+
value: ''
|
|
339
|
+
})
|
|
340
|
+
}}
|
|
341
|
+
/>
|
|
342
|
+
</div>
|
|
343
|
+
<div className='mb-2'>
|
|
344
|
+
<Select
|
|
345
|
+
value={condition.isOrIsNotEqualTo || 'is'}
|
|
346
|
+
fieldName={`condition-operator-${index}-${conditionIndex}`}
|
|
347
|
+
label='Operator'
|
|
348
|
+
options={[
|
|
349
|
+
{ value: 'is', label: 'is' },
|
|
350
|
+
{ value: 'is not', label: 'is not' }
|
|
351
|
+
]}
|
|
352
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
353
|
+
updateCondition(index, conditionIndex, { isOrIsNotEqualTo: value as 'is' | 'is not' })
|
|
354
|
+
}}
|
|
355
|
+
/>
|
|
356
|
+
</div>
|
|
357
|
+
<div className='mb-2'>
|
|
358
|
+
<Select
|
|
359
|
+
value={condition.value || ''}
|
|
360
|
+
fieldName={`condition-value-${index}-${conditionIndex}`}
|
|
361
|
+
label='Value'
|
|
362
|
+
options={[
|
|
363
|
+
{ value: '', label: 'Select Value...' },
|
|
364
|
+
...(condition.columnName
|
|
365
|
+
? getColumnValues(condition.columnName).map(val => ({
|
|
366
|
+
value: String(val),
|
|
367
|
+
label: String(val)
|
|
368
|
+
}))
|
|
369
|
+
: [])
|
|
370
|
+
]}
|
|
371
|
+
updateField={(_section, _subsection, _fieldName, value) => {
|
|
372
|
+
updateCondition(index, conditionIndex, { value })
|
|
373
|
+
}}
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
<Button
|
|
377
|
+
className='btn-sm btn-danger'
|
|
378
|
+
onClick={() => removeCondition(index, conditionIndex)}
|
|
379
|
+
>
|
|
380
|
+
Remove Condition
|
|
381
|
+
</Button>
|
|
382
|
+
</div>
|
|
383
|
+
))}
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
|
|
387
|
+
<Button
|
|
388
|
+
className='btn-sm'
|
|
389
|
+
onClick={() => addCondition(index)}
|
|
390
|
+
>
|
|
391
|
+
<Icon display='plus' size={14} className='mr-1' />
|
|
392
|
+
Add Condition
|
|
393
|
+
</Button>
|
|
394
|
+
</Accordion.Section>
|
|
395
|
+
|
|
396
|
+
<Accordion.Section title='Formatting Options'>
|
|
397
|
+
<div className='mb-3'>
|
|
398
|
+
<CheckBox
|
|
399
|
+
value={variable.addCommas || false}
|
|
400
|
+
fieldName='addCommas'
|
|
401
|
+
label='Format numbers with commas'
|
|
402
|
+
updateField={(_section, _subsection, _fieldName, value) =>
|
|
403
|
+
updateVariable(index, { addCommas: value })
|
|
404
|
+
}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div className='mb-3'>
|
|
409
|
+
<CheckBox
|
|
410
|
+
value={variable.hideOnNull || false}
|
|
411
|
+
fieldName='hideOnNull'
|
|
412
|
+
label='Hide section when value is null'
|
|
413
|
+
updateField={(_section, _subsection, _fieldName, value) =>
|
|
414
|
+
updateVariable(index, { hideOnNull: value })
|
|
415
|
+
}
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
</Accordion.Section>
|
|
419
|
+
</Accordion>
|
|
420
|
+
|
|
421
|
+
<div className='mt-3 pt-3 border-t' style={{ textAlign: 'center' }}>
|
|
422
|
+
<Button
|
|
423
|
+
className='btn-sm btn-danger'
|
|
424
|
+
onClick={() => {
|
|
425
|
+
if (window.confirm(`Are you sure you want to delete the variable "${variable.name || 'Unnamed Variable'}"?`)) {
|
|
426
|
+
removeVariable(index)
|
|
427
|
+
}
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
Delete Variable
|
|
431
|
+
</Button>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
) : <></>)}
|
|
437
|
+
</div>
|
|
438
|
+
)}
|
|
439
|
+
|
|
440
|
+
<Button className='btn-primary' onClick={addVariable}>
|
|
441
|
+
<Icon display='plus' size={16} className='mr-2' />
|
|
442
|
+
Add Variable
|
|
443
|
+
</Button>
|
|
444
|
+
</>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export default MarkupVariablesEditor
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import MarkupVariablesEditor from './MarkupVariablesEditor'
|
|
3
|
+
import Accordion from '../../ui/Accordion'
|
|
4
|
+
import { MarkupVariable } from '../../../types/MarkupVariable'
|
|
5
|
+
|
|
6
|
+
type PanelMarkupProps = {
|
|
7
|
+
/** Display name for the panel */
|
|
8
|
+
name: string
|
|
9
|
+
/** Array of markup variable configurations */
|
|
10
|
+
markupVariables: MarkupVariable[]
|
|
11
|
+
/** Dataset to extract column names and values from */
|
|
12
|
+
data: any[]
|
|
13
|
+
/** Whether markup variables feature is enabled */
|
|
14
|
+
enableMarkupVariables: boolean
|
|
15
|
+
/** Callback when variables are added, updated, or removed */
|
|
16
|
+
onMarkupVariablesChange: (variables: MarkupVariable[]) => void
|
|
17
|
+
/** Callback when enable/disable toggle changes */
|
|
18
|
+
onToggleEnable: (enabled: boolean) => void
|
|
19
|
+
/** Optional: wrap in accordion. Default true */
|
|
20
|
+
withAccordion?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shared panel for markup variables editor across all visualization packages.
|
|
25
|
+
* Wraps MarkupVariablesEditor with optional accordion functionality.
|
|
26
|
+
*/
|
|
27
|
+
const PanelMarkup: React.FC<PanelMarkupProps> = ({
|
|
28
|
+
name,
|
|
29
|
+
markupVariables,
|
|
30
|
+
data,
|
|
31
|
+
enableMarkupVariables,
|
|
32
|
+
onMarkupVariablesChange,
|
|
33
|
+
onToggleEnable,
|
|
34
|
+
withAccordion = true
|
|
35
|
+
}) => {
|
|
36
|
+
const content = (
|
|
37
|
+
<MarkupVariablesEditor
|
|
38
|
+
markupVariables={markupVariables || []}
|
|
39
|
+
data={data}
|
|
40
|
+
onChange={onMarkupVariablesChange}
|
|
41
|
+
enableMarkupVariables={enableMarkupVariables || false}
|
|
42
|
+
onToggleEnable={onToggleEnable}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (!withAccordion) {
|
|
47
|
+
return content
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Accordion key={name}>
|
|
52
|
+
<Accordion.Section title={name} key={name}>
|
|
53
|
+
{content}
|
|
54
|
+
</Accordion.Section>
|
|
55
|
+
</Accordion>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default PanelMarkup
|
|
@@ -13,7 +13,9 @@ class ErrorBoundary extends React.Component {
|
|
|
13
13
|
|
|
14
14
|
componentDidCatch(error, errorInfo) {
|
|
15
15
|
// You can also log the error to an error reporting service
|
|
16
|
-
console.
|
|
16
|
+
console.error('ErrorBoundary caught an error:', error)
|
|
17
|
+
console.error('Error info:', errorInfo)
|
|
18
|
+
console.error('Error stack:', error.stack)
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
render() {
|
|
@@ -20,6 +20,7 @@ import { applyQueuedActive } from './helpers/applyQueuedActive'
|
|
|
20
20
|
import Tabs from './components/Tabs'
|
|
21
21
|
import Dropdown from './components/Dropdown'
|
|
22
22
|
import { publishAnalyticsEvent } from '../../helpers/metrics/helpers'
|
|
23
|
+
import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
|
|
23
24
|
|
|
24
25
|
export const VIZ_FILTER_STYLE = {
|
|
25
26
|
dropdown: 'dropdown',
|
|
@@ -87,12 +88,15 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
87
88
|
const newFilters = getChangedFilters([...filters], index, value, filterBehavior)
|
|
88
89
|
setFilters(newFilters)
|
|
89
90
|
|
|
90
|
-
publishAnalyticsEvent(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`${
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
publishAnalyticsEvent({
|
|
92
|
+
vizType: visualizationConfig.type as any,
|
|
93
|
+
vizSubType: getVizSubType(visualizationConfig),
|
|
94
|
+
eventType: `${visualizationConfig.type}_filter_changed` as any,
|
|
95
|
+
eventAction: 'change',
|
|
96
|
+
eventLabel: interactionLabel,
|
|
97
|
+
vizTitle: getVizTitle(visualizationConfig),
|
|
98
|
+
specifics: `key: ${String(newFilters?.[index]?.columnName).toLowerCase()}, value: ${String(newFilters?.[index]?.active).toLowerCase()}`
|
|
99
|
+
})
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
const handleApplyButton = newFilters => {
|
|
@@ -113,17 +117,19 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
113
117
|
|
|
114
118
|
setFilters(newFilters)
|
|
115
119
|
|
|
116
|
-
publishAnalyticsEvent(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
publishAnalyticsEvent({
|
|
121
|
+
vizType: visualizationConfig.type as any,
|
|
122
|
+
eventType: `${visualizationConfig.type}_filter_applied` as any,
|
|
123
|
+
eventAction: 'click',
|
|
124
|
+
eventLabel: interactionLabel,
|
|
125
|
+
vizTitle: getVizTitle(visualizationConfig),
|
|
126
|
+
specifics: newFilters.map(f => f.active).join(',')
|
|
127
|
+
})
|
|
122
128
|
|
|
123
129
|
setShowApplyButton(false)
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
const
|
|
132
|
+
const handleFiltersReset = e => {
|
|
127
133
|
let newFilters = [...filters]
|
|
128
134
|
e.preventDefault()
|
|
129
135
|
|
|
@@ -135,10 +141,31 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
135
141
|
filter.values = getUniqueValues(visualizationConfig.data, filter.columnName)
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
|
|
144
|
+
// Determine reset value based on filter configuration
|
|
145
|
+
let resetValue
|
|
146
|
+
|
|
147
|
+
// If filter has a resetLabel, reset to empty state (shows "- Select -")
|
|
148
|
+
if (filter.resetLabel) {
|
|
149
|
+
resetValue = filter.resetLabel
|
|
150
|
+
}
|
|
151
|
+
// If filter has a defaultValue, use that
|
|
152
|
+
else if (filter.defaultValue) {
|
|
153
|
+
resetValue = filter.defaultValue
|
|
154
|
+
}
|
|
155
|
+
// Otherwise, use first value in sorted values array
|
|
156
|
+
else if (filter.values && filter.values.length > 0) {
|
|
157
|
+
resetValue = handleSorting(filter).values[0]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle multi-select filters
|
|
161
|
+
if (filter.filterStyle === 'multi-select') {
|
|
162
|
+
newFilters[i].active = resetValue ? [resetValue] : []
|
|
163
|
+
} else {
|
|
164
|
+
newFilters[i].active = resetValue
|
|
165
|
+
}
|
|
139
166
|
|
|
140
|
-
if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !==
|
|
141
|
-
queryParams[filter.setByQueryParameter] =
|
|
167
|
+
if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== newFilters[i].active) {
|
|
168
|
+
queryParams[filter.setByQueryParameter] = newFilters[i].active
|
|
142
169
|
needsQueryUpdate = true
|
|
143
170
|
}
|
|
144
171
|
})
|
|
@@ -148,12 +175,13 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
setFilters(newFilters)
|
|
151
|
-
publishAnalyticsEvent(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
178
|
+
publishAnalyticsEvent({
|
|
179
|
+
vizType: visualizationConfig.type as any,
|
|
180
|
+
eventType: `${visualizationConfig.type}_filter_reset` as any,
|
|
181
|
+
eventAction: 'click',
|
|
182
|
+
eventLabel: interactionLabel,
|
|
183
|
+
vizTitle: visualizationConfig?.title
|
|
184
|
+
})
|
|
157
185
|
}
|
|
158
186
|
|
|
159
187
|
const mobileFilterStyle = useMemo(() => {
|
|
@@ -288,9 +316,9 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
288
316
|
>
|
|
289
317
|
Apply
|
|
290
318
|
</Button>
|
|
291
|
-
<
|
|
319
|
+
<button className='btn btn-link' disabled={initialFiltersActive} onClick={handleFiltersReset}>
|
|
292
320
|
Clear Filters
|
|
293
|
-
</
|
|
321
|
+
</button>
|
|
294
322
|
</div>
|
|
295
323
|
) : (
|
|
296
324
|
<></>
|
|
@@ -10,7 +10,7 @@ type DropdownProps = {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const Dropdown: React.FC<DropdownProps> = ({ index: outerIndex, label, filter, changeFilterActive }) => {
|
|
13
|
-
const { active, queuedActive } = filter
|
|
13
|
+
const { active, queuedActive, resetLabel } = filter
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<select
|
|
@@ -25,6 +25,11 @@ const Dropdown: React.FC<DropdownProps> = ({ index: outerIndex, label, filter, c
|
|
|
25
25
|
changeFilterActive(outerIndex, e.target.value)
|
|
26
26
|
}}
|
|
27
27
|
>
|
|
28
|
+
{resetLabel && (
|
|
29
|
+
<option key='reset-option' value={resetLabel} aria-label={resetLabel}>
|
|
30
|
+
{resetLabel}
|
|
31
|
+
</option>
|
|
32
|
+
)}
|
|
28
33
|
{filter.values?.map((value, index) => {
|
|
29
34
|
return (
|
|
30
35
|
<option key={index} value={value} aria-label={value}>
|
|
@@ -41,6 +41,7 @@ const Tabs: React.FC<TabsProps> = ({ filter, index: outerIndex, changeFilterActi
|
|
|
41
41
|
const Tabs = filter.values.map((value, index) => {
|
|
42
42
|
return (
|
|
43
43
|
<button
|
|
44
|
+
key={`${value}-${outerIndex}-${index}-${id}`}
|
|
44
45
|
id={`${value}-${outerIndex}-${index}-${id}`}
|
|
45
46
|
className={getClassList(value)}
|
|
46
47
|
onClick={e => {
|