@cdc/core 4.25.10 → 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.
Files changed (73) hide show
  1. package/_stories/StoryRenderingTests.stories.tsx +164 -0
  2. package/components/AdvancedEditor/AdvancedEditor.tsx +3 -1
  3. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  4. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  5. package/components/CustomColorsEditor/index.ts +1 -0
  6. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  7. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  8. package/components/DataTable/data-table.css +6 -0
  9. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -3
  10. package/components/DataTable/helpers/standardizeState.js +2 -2
  11. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  12. package/components/EditorPanel/DataTableEditor.tsx +3 -3
  13. package/components/EditorPanel/EditorPanel.styles.css +423 -0
  14. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  15. package/components/EditorPanel/Inputs.tsx +12 -2
  16. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  17. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -2
  18. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
  19. package/components/Filters/Filters.tsx +26 -5
  20. package/components/Filters/components/Dropdown.tsx +6 -1
  21. package/components/Footnotes/Footnotes.tsx +35 -25
  22. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  23. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  24. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  25. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  26. package/components/HeaderThemeSelector/index.ts +2 -0
  27. package/components/Layout/styles/editor.scss +2 -1
  28. package/components/Loader/Loader.tsx +1 -1
  29. package/components/MediaControls.tsx +21 -18
  30. package/components/PaletteConversionModal.tsx +7 -4
  31. package/components/PaletteSelector/PaletteSelector.css +49 -6
  32. package/components/Table/components/Cell.tsx +23 -2
  33. package/components/Table/components/Row.tsx +5 -3
  34. package/components/_stories/Filters.stories.tsx +20 -1
  35. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  36. package/components/_stories/Footnotes.stories.tsx +768 -3
  37. package/components/_stories/Inputs.stories.tsx +2 -2
  38. package/components/_stories/styles.scss +0 -1
  39. package/components/ui/Accordion.jsx +1 -1
  40. package/components/ui/accordion.styles.css +57 -0
  41. package/data/chartColorPalettes.ts +1 -1
  42. package/dist/cove-main.css +49 -3
  43. package/dist/cove-main.css.map +1 -1
  44. package/helpers/addValuesToFilters.ts +5 -0
  45. package/helpers/constants.ts +37 -0
  46. package/helpers/cove/number.ts +33 -12
  47. package/helpers/coveUpdateWorker.ts +3 -1
  48. package/helpers/fetchRemoteData.ts +3 -15
  49. package/helpers/markupProcessor.ts +27 -12
  50. package/helpers/mergeCustomOrderValues.ts +37 -0
  51. package/helpers/parseCsvWithQuotes.ts +65 -0
  52. package/helpers/testing.ts +17 -4
  53. package/helpers/ver/4.25.11.ts +13 -0
  54. package/helpers/viewports.ts +2 -0
  55. package/package.json +4 -3
  56. package/styles/_common-components.css +73 -0
  57. package/styles/_global.scss +25 -5
  58. package/styles/base.scss +0 -50
  59. package/styles/cove-main.scss +3 -1
  60. package/styles/filters.scss +10 -3
  61. package/styles/v2/base/index.scss +0 -1
  62. package/styles/v2/components/editor.scss +14 -6
  63. package/styles/v2/utils/_breakpoints.scss +1 -1
  64. package/styles/v2/utils/index.scss +0 -1
  65. package/styles/waiting.scss +1 -1
  66. package/types/MarkupInclude.ts +4 -3
  67. package/types/MarkupVariable.ts +1 -1
  68. package/types/VizFilter.ts +1 -0
  69. package/styles/_mixins.scss +0 -13
  70. package/styles/_typography.scss +0 -0
  71. package/styles/v2/base/_typography.scss +0 -0
  72. package/styles/v2/components/guidance-block.scss +0 -74
  73. package/styles/v2/utils/_functions.scss +0 -0
@@ -3,8 +3,7 @@ import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
3
3
  import _ from 'lodash'
4
4
  import Footnotes, { Footnote } from '../../types/Footnotes'
5
5
  import { footnotesSymbols } from '../../helpers/footnoteSymbols'
6
- import InputSelect from '../inputs/InputSelect'
7
- import { TextField } from './Inputs'
6
+ import { TextField, Select } from './Inputs'
8
7
  import { Datasets } from '@cdc/core/types/DataSet'
9
8
  import DataTransform from '../../helpers/DataTransform'
10
9
  import fetchRemoteData from '../../helpers/fetchRemoteData'
@@ -61,14 +60,14 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
61
60
  updateField('footnotes', null, 'staticFootnotes', footnoteCopy)
62
61
  }
63
62
 
64
- const getOptions = (opts: string[]) => {
65
- return [['', '--Select--']].concat(opts.map(key => [key, key]))
63
+ const getSelectOptions = (opts: string[]) => {
64
+ return [{ value: '', label: '--Select--' }].concat(opts.map(key => ({ value: key, label: key })))
66
65
  }
67
66
 
68
67
  const dataColumns = footnotesConfig.dataKey
69
- ? getOptions(Object.keys(datasetsCache[footnotesConfig.dataKey]?.data?.[0] || {}))
68
+ ? getSelectOptions(Object.keys(datasetsCache[footnotesConfig.dataKey]?.data?.[0] || {}))
70
69
  : []
71
- const dataSetOptions = getOptions(Object.keys(datasetsCache))
70
+ const dataSetOptions = getSelectOptions(Object.keys(datasetsCache))
72
71
 
73
72
  const changeFootnoteDataKey = async value => {
74
73
  if (value) {
@@ -87,7 +86,7 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
87
86
  {loadingAPIData && <Loader fullScreen />}
88
87
  <em>Dynamic Footnotes</em>
89
88
  <div className='row border p-2'>
90
- <InputSelect
89
+ <Select
91
90
  label='Select a Footnote Dataset'
92
91
  value={footnotesConfig.dataKey}
93
92
  options={dataSetOptions}
@@ -100,7 +99,7 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
100
99
 
101
100
  {footnotesConfig.dataKey && (
102
101
  <div className='p-3'>
103
- <InputSelect
102
+ <Select
104
103
  label='Footnote Symbol Column'
105
104
  value={footnotesConfig.dynamicFootnotes?.symbolColumn}
106
105
  options={dataColumns}
@@ -109,7 +108,7 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
109
108
  fieldName='symbolColumn'
110
109
  updateField={updateField}
111
110
  />
112
- <InputSelect
111
+ <Select
113
112
  label='Footnote Text Column'
114
113
  value={footnotesConfig.dynamicFootnotes?.textColumn}
115
114
  options={dataColumns}
@@ -118,7 +117,7 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
118
117
  fieldName='textColumn'
119
118
  updateField={updateField}
120
119
  />
121
- <InputSelect
120
+ <Select
122
121
  label='Footnote Order Column'
123
122
  value={footnotesConfig.dynamicFootnotes?.orderColumn}
124
123
  options={dataColumns}
@@ -135,34 +134,42 @@ const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField,
135
134
 
136
135
  <em>Static Footnotes</em>
137
136
 
138
- {footnotesConfig.staticFootnotes?.map((note, index) => (
139
- <div key={index} className='row border p-2'>
140
- <div className='col-8'>
141
- <InputSelect
142
- label='Symbol'
143
- value={note.symbol}
144
- options={[['', '--Select--'], ...footnotesSymbols]}
145
- fieldName='symbol'
146
- updateField={(section, subsection, fieldName, value) =>
147
- updateStaticFootnote(index, { ...note, symbol: value })
148
- }
149
- />{' '}
150
- <TextField
151
- label='Text'
152
- value={note.text}
153
- fieldName='text'
154
- updateField={(section, subsection, fieldName, value) =>
155
- updateStaticFootnote(index, { ...note, text: value })
156
- }
157
- />
158
- </div>
159
- <div className='col-2 ms-4'>
160
- <button className='btn btn-danger p-1' onClick={() => deleteStaticFootnote(index)}>
161
- Delete
162
- </button>
137
+ {footnotesConfig.staticFootnotes?.map((note, index) => {
138
+ // Convert tuple format to {value, label} format for Select component
139
+ const symbolOptions = [
140
+ { value: '', label: '--Select--' },
141
+ ...footnotesSymbols.map(([value, label]) => ({ value, label }))
142
+ ]
143
+
144
+ return (
145
+ <div key={index} className='row border p-2'>
146
+ <div className='col-8'>
147
+ <Select
148
+ label='Symbol'
149
+ value={note.symbol}
150
+ options={symbolOptions}
151
+ fieldName='symbol'
152
+ updateField={(section, subsection, fieldName, value) =>
153
+ updateStaticFootnote(index, { ...note, symbol: value })
154
+ }
155
+ />{' '}
156
+ <TextField
157
+ label='Text'
158
+ value={note.text}
159
+ fieldName='text'
160
+ updateField={(section, subsection, fieldName, value) =>
161
+ updateStaticFootnote(index, { ...note, text: value })
162
+ }
163
+ />
164
+ </div>
165
+ <div className='col-2 ms-4'>
166
+ <button className='btn btn-danger p-1' onClick={() => deleteStaticFootnote(index)}>
167
+ Delete
168
+ </button>
169
+ </div>
163
170
  </div>
164
- </div>
165
- ))}
171
+ )
172
+ })}
166
173
  <button className='btn btn-primary' onClick={addStaticFootnote}>
167
174
  Add Static Footnote
168
175
  </button>
@@ -149,6 +149,7 @@ export type SelectProps = {
149
149
  options?: string[] | { label: string; value: string }[]
150
150
  required?: boolean
151
151
  initial?: string
152
+ disabled?: boolean
152
153
 
153
154
  // all other props
154
155
  [x: string]: any
@@ -167,6 +168,8 @@ const Select = memo((props: SelectProps) => {
167
168
  tooltip,
168
169
  updateField,
169
170
  initial: initialValue,
171
+ disabled = false,
172
+ onChange: onChangeProp,
170
173
  ...attributes
171
174
  } = props
172
175
  const optionsJsx = options?.map((option, index) => {
@@ -197,7 +200,7 @@ const Select = memo((props: SelectProps) => {
197
200
  }
198
201
 
199
202
  return (
200
- <label>
203
+ <label style={disabled ? { opacity: 0.6, pointerEvents: 'none' } : {}}>
201
204
  <span className='edit-label'>
202
205
  {label}
203
206
  {tooltip}
@@ -206,9 +209,16 @@ const Select = memo((props: SelectProps) => {
206
209
  className={`cove-form-select ${required && !value ? 'warning' : ''} ${DROPDOWN_STYLES}`}
207
210
  name={fieldName}
208
211
  value={value}
212
+ disabled={disabled}
209
213
  onChange={event => {
210
- updateField(section, subsection, fieldName, event.target.value)
214
+ if (updateField) {
215
+ updateField(section, subsection, fieldName, event.target.value)
216
+ }
217
+ if (onChangeProp) {
218
+ onChangeProp(event)
219
+ }
211
220
  }}
221
+ style={disabled ? { cursor: 'not-allowed', backgroundColor: '#e9ecef' } : {}}
212
222
  {...attributes}
213
223
  >
214
224
  {optionsJsx}
@@ -5,6 +5,7 @@ import { filterOrderOptions } from '../../../helpers/filterOrderOptions'
5
5
  import FilterOrder from './components/FilterOrder'
6
6
  import { Visualization } from '../../../types/Visualization'
7
7
  import { useMemo } from 'react'
8
+ import { Select } from '../Inputs'
8
9
 
9
10
  type NestedDropdownEditorProps = {
10
11
  config: Visualization
@@ -155,43 +156,26 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
155
156
  />
156
157
  </label>
157
158
 
158
- <label>
159
- <div className='edit-label column-heading mt-2'>
160
- Filter Grouping
161
- <span></span>
162
- </div>
163
- <select value={filter.columnName} onChange={e => handleGroupColumnNameChange(e.target.value)}>
164
- <option value=''>- Select Option -</option>
165
- {columnNameOptions.map((option, index) => (
166
- <option value={option} key={index}>
167
- {option}
168
- </option>
169
- ))}
170
- </select>
171
- </label>
172
- <label>
173
- <div className='edit-label column-heading mt-2'>
174
- Filter SubGrouping
175
- <span></span>
176
- </div>
177
- <select
178
- value={subGrouping?.columnName ?? ''}
179
- onChange={e => {
180
- handleSubGroupColumnNameChange(e.target.value)
181
- }}
182
- >
183
- <option value=''>- Select Option -</option>
184
- {columnNameOptions.map((option, index) => {
185
- if (option !== filter.columnName) {
186
- return (
187
- <option value={option} key={index}>
188
- {option}
189
- </option>
190
- )
191
- }
192
- })}
193
- </select>
194
- </label>
159
+ <Select
160
+ label='Filter Grouping'
161
+ value={filter.columnName}
162
+ options={[{ value: '', label: '- Select Option -' }, ...columnNameOptions.map(opt => ({ value: opt, label: opt }))]}
163
+ onChange={e => handleGroupColumnNameChange(e.target.value)}
164
+ />
165
+
166
+ <Select
167
+ label='Filter SubGrouping'
168
+ value={subGrouping?.columnName ?? ''}
169
+ options={[
170
+ { value: '', label: '- Select Option -' },
171
+ ...columnNameOptions
172
+ .filter(option => option !== filter.columnName)
173
+ .map(opt => ({ value: opt, label: opt }))
174
+ ]}
175
+ onChange={e => {
176
+ handleSubGroupColumnNameChange(e.target.value)
177
+ }}
178
+ />
195
179
 
196
180
  <label>
197
181
  <input
@@ -229,39 +213,28 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
229
213
  )}
230
214
  </label>
231
215
 
232
- <label className='mt-2'>
216
+ <div className='mt-2'>
233
217
  <div className='edit-label column-heading float-right'>{filter.columnName} </div>
234
- <span className={'edit-filterOrder column-heading '}>Group Order</span>
235
- <select value={filter.order} onChange={e => handleGroupingOrderBy(e.target.value as OrderBy)}>
236
- {filterOrderOptions.map((option, index) => {
237
- return (
238
- <option value={option.value} key={`filter-${option.label}-${index}`}>
239
- {option.label}
240
- </option>
241
- )
242
- })}
243
- </select>
218
+ <Select
219
+ label='Group Order'
220
+ value={filter.order}
221
+ options={filterOrderOptions}
222
+ onChange={e => handleGroupingOrderBy(e.target.value as OrderBy)}
223
+ />
244
224
  {filter.order === 'cust' && (
245
225
  <FilterOrder orderedValues={filter.orderedValues} handleFilterOrder={handleGroupingCustomOrder} />
246
226
  )}
247
- </label>
227
+ </div>
248
228
 
249
229
  {subGrouping?.columnName && (
250
- <label className='mt-2'>
251
- <span className={'edit-filterOrder column-heading'}>SubGrouping Order</span>
230
+ <div className='mt-2'>
252
231
  <div className='edit-label column-heading float-right'>{subGrouping.columnName} </div>
253
- <select
232
+ <Select
233
+ label='SubGrouping Order'
254
234
  value={subGrouping.order ? subGrouping.order : 'asc'}
235
+ options={filterOrderOptions}
255
236
  onChange={e => handleSubGroupingOrderBy(e.target.value as OrderBy)}
256
- >
257
- {filterOrderOptions.map((option, index) => {
258
- return (
259
- <option value={option.value} key={`filter-${index}`}>
260
- {option.label}
261
- </option>
262
- )
263
- })}
264
- </select>
237
+ />
265
238
  {subGrouping?.order === 'cust' &&
266
239
  filter.values.map((groupName, i) => {
267
240
  const orderedSubGroupValues = subGrouping.valuesLookup[groupName].orderedValues
@@ -278,7 +251,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
278
251
  </div>
279
252
  )
280
253
  })}
281
- </label>
254
+ </div>
282
255
  )}
283
256
  </div>
284
257
  )
@@ -28,6 +28,16 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
28
28
  return _.uniq(_.flatten(rawData?.map(row => Object.keys(row))))
29
29
  }, [rawData])
30
30
 
31
+ // Helper function to get filter values from various sources
32
+ const getFilterValues = (filter: VizFilter) => {
33
+ if (filter.values && filter.values.length > 0) return filter.values
34
+ if (filter.orderedValues && filter.orderedValues.length > 0) return filter.orderedValues
35
+ if (filter.columnName && rawData && rawData.length > 0) {
36
+ return _.uniq(rawData.map(row => row[filter.columnName]))
37
+ }
38
+ return []
39
+ }
40
+
31
41
  const removeFilter = index => {
32
42
  let filters = _.cloneDeep(config.filters)
33
43
 
@@ -188,8 +198,8 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
188
198
  value={filter.defaultValue}
189
199
  options={
190
200
  filter.resetLabel
191
- ? [filter.resetLabel, ...config.filters?.[filterIndex].values]
192
- : config.filters?.[filterIndex].values
201
+ ? [filter.resetLabel, ...getFilterValues(filter)]
202
+ : getFilterValues(filter)
193
203
  }
194
204
  updateField={(_section, _subSection, _key, value) => {
195
205
  updateFilterDefaultValue(filterIndex, value)
@@ -1,15 +1,21 @@
1
- import React, { useState, useMemo } from 'react'
1
+ import React, { useState, useMemo, useCallback } from 'react'
2
2
  import { MarkupVariable, MarkupCondition } from '../../../types/MarkupVariable'
3
3
  import Button from '../../elements/Button'
4
4
  import { TextField, Select, CheckBox } from '../Inputs'
5
5
  import Icon from '../../ui/Icon'
6
6
  import Accordion from '../../ui/Accordion'
7
+ import { Datasets } from '../../../types/DataSet'
8
+ import _ from 'lodash'
7
9
 
8
10
  type MarkupVariablesEditorProps = {
9
11
  /** Array of markup variable configurations */
10
12
  markupVariables: MarkupVariable[]
11
- /** Dataset to extract column names and values from */
12
- data: any[]
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 }
13
19
  /** Callback when variables are added, updated, or removed */
14
20
  onChange: (variables: MarkupVariable[]) => void
15
21
  /** Whether markup variables feature is enabled */
@@ -27,6 +33,8 @@ export type { MarkupVariablesEditorProps }
27
33
  const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
28
34
  markupVariables = [],
29
35
  data = [],
36
+ datasets,
37
+ config,
30
38
  onChange,
31
39
  enableMarkupVariables = false,
32
40
  onToggleEnable
@@ -34,9 +42,46 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
34
42
  const [editingIndex, setEditingIndex] = useState<number | null>(null)
35
43
  const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({})
36
44
 
37
- // Ensure we always have a valid array
38
- const safeMarkupVariables = markupVariables || []
39
- const availableColumns = data.length > 0 ? Object.keys(data[0]) : []
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])
40
85
 
41
86
  // Validate a variable and return array of error messages
42
87
  const validateVariable = React.useCallback((variable: MarkupVariable): string[] => {
@@ -71,7 +116,12 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
71
116
  errors[index] = variableErrors
72
117
  }
73
118
  })
74
- setValidationErrors(errors)
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
+ })
75
125
  }, [safeMarkupVariables, validateVariable]) // Re-validate when variables change
76
126
 
77
127
 
@@ -125,18 +175,7 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
125
175
  return `{{${name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')}}}`
126
176
  }
127
177
 
128
- // Get unique values for a given column for condition dropdowns
129
- const getColumnValues = useMemo(() => {
130
- if (!data || data.length === 0) return {}
131
178
 
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
179
 
141
180
  const addCondition = (variableIndex: number) => {
142
181
  const variable = safeMarkupVariables[variableIndex]
@@ -265,7 +304,7 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
265
304
  label='Data Column'
266
305
  options={[
267
306
  { value: '', label: 'Select Column...' },
268
- ...availableColumns.map(col => ({ value: col, label: col }))
307
+ ...getAvailableColumns.map(col => ({ value: col, label: col }))
269
308
  ]}
270
309
  updateField={(_section, _subsection, _fieldName, value) => {
271
310
  updateVariable(index, { columnName: value })
@@ -290,7 +329,7 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
290
329
  label='Column'
291
330
  options={[
292
331
  { value: '', label: 'Select Column...' },
293
- ...availableColumns.map(col => ({ value: col, label: col }))
332
+ ...getAvailableColumns.map(col => ({ value: col, label: col }))
294
333
  ]}
295
334
  updateField={(_section, _subsection, _fieldName, newColumnName) => {
296
335
  // Reset value when column changes
@@ -322,8 +361,8 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
322
361
  label='Value'
323
362
  options={[
324
363
  { value: '', label: 'Select Value...' },
325
- ...(condition.columnName && getColumnValues[condition.columnName]
326
- ? getColumnValues[condition.columnName].map(val => ({
364
+ ...(condition.columnName
365
+ ? getColumnValues(condition.columnName).map(val => ({
327
366
  value: String(val),
328
367
  label: String(val)
329
368
  }))
@@ -141,10 +141,31 @@ const Filters: React.FC<FilterProps> = ({
141
141
  filter.values = getUniqueValues(visualizationConfig.data, filter.columnName)
142
142
  }
143
143
 
144
- newFilters[i].active = handleSorting(filter).values[0]
144
+ // Determine reset value based on filter configuration
145
+ let resetValue
145
146
 
146
- if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
147
- queryParams[filter.setByQueryParameter] = filter.active
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
+ }
166
+
167
+ if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== newFilters[i].active) {
168
+ queryParams[filter.setByQueryParameter] = newFilters[i].active
148
169
  needsQueryUpdate = true
149
170
  }
150
171
  })
@@ -295,9 +316,9 @@ const Filters: React.FC<FilterProps> = ({
295
316
  >
296
317
  Apply
297
318
  </Button>
298
- <Button secondary disabled={initialFiltersActive} onClick={handleFiltersReset}>
319
+ <button className='btn btn-link' disabled={initialFiltersActive} onClick={handleFiltersReset}>
299
320
  Clear Filters
300
- </Button>
321
+ </button>
301
322
  </div>
302
323
  ) : (
303
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}>
@@ -1,25 +1,35 @@
1
- import { Footnote } from '../../types/Footnotes'
2
- import './footnotes.css'
3
-
4
- type FootnotesProps = {
5
- footnotes: Footnote[]
6
- }
7
-
8
- const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
9
- return (
10
- <footer className='col-12 m-3 mt-1 mb-0'>
11
- <ul className='cove-footnotes'>
12
- {footnotes.map((note, i) => {
13
- return (
14
- <li key={`${note.symbol || 'footnote-'}${i}`} className='mb-1'>
15
- {note.symbol && <span className='me-1'>{note.symbol}</span>}
16
- {note.text}
17
- </li>
18
- )
19
- })}
20
- </ul>
21
- </footer>
22
- )
23
- }
24
-
25
- export default Footnotes
1
+ import { Footnote } from '../../types/Footnotes'
2
+ import parse from 'html-react-parser'
3
+ import './footnotes.css'
4
+
5
+ type FootnotesProps = {
6
+ footnotes: Footnote[]
7
+ }
8
+
9
+ const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
10
+ // Convert newlines to <br> tags and parse HTML
11
+ const processFootnoteText = (text: string) => {
12
+ if (!text) return ''
13
+ // Convert newline characters to <br> tags
14
+ const textWithBreaks = text.replace(/\n/g, '<br>')
15
+ // Parse HTML (html-react-parser handles sanitization)
16
+ return parse(textWithBreaks)
17
+ }
18
+
19
+ return (
20
+ <footer className='col-12 m-3 mt-1 mb-0'>
21
+ <ul className='cove-footnotes'>
22
+ {footnotes.map((note, i) => {
23
+ return (
24
+ <li key={`${note.symbol || 'footnote-'}${i}`} className='mb-1'>
25
+ {note.symbol && <span className='me-1'>{note.symbol}</span>}
26
+ {processFootnoteText(note.text)}
27
+ </li>
28
+ )
29
+ })}
30
+ </ul>
31
+ </footer>
32
+ )
33
+ }
34
+
35
+ export default Footnotes