@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.
- package/_stories/StoryRenderingTests.stories.tsx +164 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +3 -1
- package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
- package/components/CustomColorsEditor/index.ts +1 -0
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/data-table.css +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +14 -3
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- 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/MarkupVariablesEditor.tsx +61 -22
- package/components/Filters/Filters.tsx +26 -5
- package/components/Filters/components/Dropdown.tsx +6 -1
- 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/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +21 -18
- package/components/PaletteConversionModal.tsx +7 -4
- package/components/PaletteSelector/PaletteSelector.css +49 -6
- package/components/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/Filters.stories.tsx +20 -1
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +768 -3
- package/components/_stories/Inputs.stories.tsx +2 -2
- package/components/_stories/styles.scss +0 -1
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/accordion.styles.css +57 -0
- package/data/chartColorPalettes.ts +1 -1
- package/dist/cove-main.css +49 -3
- package/dist/cove-main.css.map +1 -1
- package/helpers/addValuesToFilters.ts +5 -0
- package/helpers/constants.ts +37 -0
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +3 -1
- package/helpers/fetchRemoteData.ts +3 -15
- package/helpers/markupProcessor.ts +27 -12
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/parseCsvWithQuotes.ts +65 -0
- package/helpers/testing.ts +17 -4
- package/helpers/ver/4.25.11.ts +13 -0
- package/helpers/viewports.ts +2 -0
- package/package.json +4 -3
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +25 -5
- package/styles/base.scss +0 -50
- 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/editor.scss +14 -6
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/types/MarkupInclude.ts +4 -3
- package/types/MarkupVariable.ts +1 -1
- package/types/VizFilter.ts +1 -0
- package/styles/_mixins.scss +0 -13
- package/styles/_typography.scss +0 -0
- 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
|
@@ -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
|
|
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
|
|
65
|
-
return [
|
|
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
|
-
?
|
|
68
|
+
? getSelectOptions(Object.keys(datasetsCache[footnotesConfig.dataKey]?.data?.[0] || {}))
|
|
70
69
|
: []
|
|
71
|
-
const dataSetOptions =
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
<
|
|
216
|
+
<div className='mt-2'>
|
|
233
217
|
<div className='edit-label column-heading float-right'>{filter.columnName} </div>
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
{
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
</
|
|
227
|
+
</div>
|
|
248
228
|
|
|
249
229
|
{subGrouping?.columnName && (
|
|
250
|
-
<
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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, ...
|
|
192
|
-
:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
|
326
|
-
? getColumnValues
|
|
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
|
-
|
|
144
|
+
// Determine reset value based on filter configuration
|
|
145
|
+
let resetValue
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
<
|
|
319
|
+
<button className='btn btn-link' disabled={initialFiltersActive} onClick={handleFiltersReset}>
|
|
299
320
|
Clear Filters
|
|
300
|
-
</
|
|
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 '
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|