@cdc/core 4.25.8 → 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 +56 -38
- package/components/DataTable/components/ChartHeader.tsx +44 -14
- package/components/DataTable/components/ExpandCollapse.tsx +10 -1
- package/components/DataTable/components/MapHeader.tsx +24 -13
- 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 +40 -14
- 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 +27 -20
- package/components/Filters/components/Tabs.tsx +1 -0
- package/components/Legend/Legend.Gradient.tsx +3 -6
- package/components/LegendShape.tsx +121 -3
- package/components/MediaControls.tsx +51 -3
- 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 -11
- 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/coveUpdateWorker.ts +13 -3
- 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 +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/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 +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/hooks/useColorPalette.ts +79 -0
- package/package.json +12 -4
- 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/MarkupInclude.ts +6 -1
- package/types/MarkupVariable.ts +19 -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/formatConfigBeforeSave.ts +0 -135
- package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { processMarkupVariables, validateMarkupVariables } from '../markupProcessor'
|
|
2
|
+
import { MarkupVariable } from '../../types/MarkupVariable'
|
|
3
|
+
import { expect, describe, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
describe('processMarkupVariables', () => {
|
|
6
|
+
const testData = [
|
|
7
|
+
{ state: 'California', population: '39538223', year: '2023' },
|
|
8
|
+
{ state: 'Texas', population: '29145505', year: '2023' },
|
|
9
|
+
{ state: 'Florida', population: '21538187', year: '2023' }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
describe('Basic Variable Substitution', () => {
|
|
13
|
+
it('should replace simple variable tags with data values', () => {
|
|
14
|
+
const variables: MarkupVariable[] = [
|
|
15
|
+
{
|
|
16
|
+
name: 'State',
|
|
17
|
+
tag: '{{state}}',
|
|
18
|
+
columnName: 'state',
|
|
19
|
+
conditions: []
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const content = 'The state is {{state}}'
|
|
24
|
+
const result = processMarkupVariables(content, testData, variables)
|
|
25
|
+
|
|
26
|
+
expect(result.processedContent).toBe('The state is California, Texas, and Florida')
|
|
27
|
+
expect(result.shouldHideSection).toBe(false)
|
|
28
|
+
expect(result.shouldShowNoDataMessage).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should handle multiple variables in same content', () => {
|
|
32
|
+
const variables: MarkupVariable[] = [
|
|
33
|
+
{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] },
|
|
34
|
+
{ name: 'Year', tag: '{{year}}', columnName: 'year', conditions: [] }
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const content = 'Data for {{state}} in {{year}}'
|
|
38
|
+
const result = processMarkupVariables(content, testData, variables)
|
|
39
|
+
|
|
40
|
+
expect(result.processedContent).toContain('California, Texas, and Florida')
|
|
41
|
+
expect(result.processedContent).toContain('2023')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should return original content if no variables defined', () => {
|
|
45
|
+
const content = 'Text with {{undefined}}'
|
|
46
|
+
const result = processMarkupVariables(content, testData, [])
|
|
47
|
+
|
|
48
|
+
expect(result.processedContent).toBe('Text with {{undefined}}')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should leave unknown variable tags unchanged', () => {
|
|
52
|
+
const variables: MarkupVariable[] = [
|
|
53
|
+
{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const content = 'Known: {{state}}, Unknown: {{unknown}}'
|
|
57
|
+
const result = processMarkupVariables(content, testData, variables)
|
|
58
|
+
|
|
59
|
+
expect(result.processedContent).toContain('California, Texas, and Florida')
|
|
60
|
+
expect(result.processedContent).toContain('{{unknown}}')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('Number Formatting with Commas', () => {
|
|
65
|
+
it('should add commas to numbers when addCommas is true', () => {
|
|
66
|
+
const variables: MarkupVariable[] = [
|
|
67
|
+
{
|
|
68
|
+
name: 'Population',
|
|
69
|
+
tag: '{{population}}',
|
|
70
|
+
columnName: 'population',
|
|
71
|
+
conditions: [],
|
|
72
|
+
addCommas: true
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
const content = 'Population: {{population}}'
|
|
77
|
+
const result = processMarkupVariables(content, testData, variables)
|
|
78
|
+
|
|
79
|
+
expect(result.processedContent).toContain('39,538,223')
|
|
80
|
+
expect(result.processedContent).toContain('29,145,505')
|
|
81
|
+
expect(result.processedContent).toContain('21,538,187')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should not add commas when addCommas is false', () => {
|
|
85
|
+
const variables: MarkupVariable[] = [
|
|
86
|
+
{
|
|
87
|
+
name: 'Population',
|
|
88
|
+
tag: '{{population}}',
|
|
89
|
+
columnName: 'population',
|
|
90
|
+
conditions: [],
|
|
91
|
+
addCommas: false
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const content = 'Population: {{population}}'
|
|
96
|
+
const result = processMarkupVariables(content, testData, variables)
|
|
97
|
+
|
|
98
|
+
expect(result.processedContent).toContain('39538223')
|
|
99
|
+
expect(result.processedContent).not.toContain('39,538,223')
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('Conditional Filtering', () => {
|
|
104
|
+
it('should filter data with single "is" condition', () => {
|
|
105
|
+
const variables: MarkupVariable[] = [
|
|
106
|
+
{
|
|
107
|
+
name: 'State',
|
|
108
|
+
tag: '{{state}}',
|
|
109
|
+
columnName: 'state',
|
|
110
|
+
conditions: [
|
|
111
|
+
{ columnName: 'state', isOrIsNotEqualTo: 'is', value: 'California' }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
const content = 'The state is {{state}}'
|
|
117
|
+
const result = processMarkupVariables(content, testData, variables)
|
|
118
|
+
|
|
119
|
+
expect(result.processedContent).toBe('The state is California')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should filter data with single "is not" condition', () => {
|
|
123
|
+
const variables: MarkupVariable[] = [
|
|
124
|
+
{
|
|
125
|
+
name: 'State',
|
|
126
|
+
tag: '{{state}}',
|
|
127
|
+
columnName: 'state',
|
|
128
|
+
conditions: [
|
|
129
|
+
{ columnName: 'state', isOrIsNotEqualTo: 'is not', value: 'California' }
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const content = 'States: {{state}}'
|
|
135
|
+
const result = processMarkupVariables(content, testData, variables)
|
|
136
|
+
|
|
137
|
+
expect(result.processedContent).toBe('States: Texas and Florida')
|
|
138
|
+
expect(result.processedContent).not.toContain('California')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should handle multiple AND conditions', () => {
|
|
142
|
+
const dataWithYears = [
|
|
143
|
+
{ state: 'California', year: '2022' },
|
|
144
|
+
{ state: 'California', year: '2023' },
|
|
145
|
+
{ state: 'Texas', year: '2023' }
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
const variables: MarkupVariable[] = [
|
|
149
|
+
{
|
|
150
|
+
name: 'State',
|
|
151
|
+
tag: '{{state}}',
|
|
152
|
+
columnName: 'state',
|
|
153
|
+
conditions: [
|
|
154
|
+
{ columnName: 'state', isOrIsNotEqualTo: 'is', value: 'California' },
|
|
155
|
+
{ columnName: 'year', isOrIsNotEqualTo: 'is', value: '2023' }
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
const content = 'State: {{state}}'
|
|
161
|
+
const result = processMarkupVariables(content, dataWithYears, variables)
|
|
162
|
+
|
|
163
|
+
expect(result.processedContent).toBe('State: California')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should return empty string when no data matches conditions', () => {
|
|
167
|
+
const variables: MarkupVariable[] = [
|
|
168
|
+
{
|
|
169
|
+
name: 'State',
|
|
170
|
+
tag: '{{state}}',
|
|
171
|
+
columnName: 'state',
|
|
172
|
+
conditions: [
|
|
173
|
+
{ columnName: 'state', isOrIsNotEqualTo: 'is', value: 'NonExistent' }
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
const content = 'State: {{state}}'
|
|
179
|
+
const result = processMarkupVariables(content, testData, variables)
|
|
180
|
+
|
|
181
|
+
expect(result.processedContent).toBe('State: ')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('Empty Values and Null Handling', () => {
|
|
186
|
+
it('should filter out empty string values', () => {
|
|
187
|
+
const dataWithEmpty = [
|
|
188
|
+
{ name: 'Alice' },
|
|
189
|
+
{ name: '' },
|
|
190
|
+
{ name: 'Bob' },
|
|
191
|
+
{ name: '' }
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
const variables: MarkupVariable[] = [
|
|
195
|
+
{ name: 'Name', tag: '{{name}}', columnName: 'name', conditions: [] }
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
const content = 'Names: {{name}}'
|
|
199
|
+
const result = processMarkupVariables(content, dataWithEmpty, variables)
|
|
200
|
+
|
|
201
|
+
expect(result.processedContent).toBe('Names: Alice and Bob')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should handle null values gracefully', () => {
|
|
205
|
+
const dataWithNull = [
|
|
206
|
+
{ value: 'A' },
|
|
207
|
+
{ value: null },
|
|
208
|
+
{ value: 'B' }
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
const variables: MarkupVariable[] = [
|
|
212
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
const content = 'Values: {{value}}'
|
|
216
|
+
const result = processMarkupVariables(content, dataWithNull, variables)
|
|
217
|
+
|
|
218
|
+
expect(result.processedContent).toBe('Values: A and B')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should handle undefined values', () => {
|
|
222
|
+
const dataWithUndefined = [
|
|
223
|
+
{ value: 'A' },
|
|
224
|
+
{ value: undefined },
|
|
225
|
+
{ value: 'B' }
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
const variables: MarkupVariable[] = [
|
|
229
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
const content = 'Values: {{value}}'
|
|
233
|
+
const result = processMarkupVariables(content, dataWithUndefined, variables)
|
|
234
|
+
|
|
235
|
+
expect(result.processedContent).toBe('Values: A and B')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should return empty when all values are empty/null', () => {
|
|
239
|
+
const dataEmpty = [
|
|
240
|
+
{ value: '' },
|
|
241
|
+
{ value: null },
|
|
242
|
+
{ value: undefined }
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
const variables: MarkupVariable[] = [
|
|
246
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
const content = 'Values: {{value}}'
|
|
250
|
+
const result = processMarkupVariables(content, dataEmpty, variables)
|
|
251
|
+
|
|
252
|
+
expect(result.processedContent).toBe('Values: ')
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('List Formatting', () => {
|
|
257
|
+
it('should format two items with "and" in production mode', () => {
|
|
258
|
+
const data = [{ state: 'CA' }, { state: 'TX' }]
|
|
259
|
+
const variables: MarkupVariable[] = [
|
|
260
|
+
{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
const content = '{{state}}'
|
|
264
|
+
const result = processMarkupVariables(content, data, variables, { isEditor: false })
|
|
265
|
+
|
|
266
|
+
expect(result.processedContent).toBe('CA and TX')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('should format three items with Oxford comma and "and"', () => {
|
|
270
|
+
const data = [{ state: 'CA' }, { state: 'TX' }, { state: 'FL' }]
|
|
271
|
+
const variables: MarkupVariable[] = [
|
|
272
|
+
{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
const content = '{{state}}'
|
|
276
|
+
const result = processMarkupVariables(content, data, variables, { isEditor: false })
|
|
277
|
+
|
|
278
|
+
expect(result.processedContent).toBe('CA, TX, and FL')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('should use "or" conjunction in editor mode', () => {
|
|
282
|
+
const data = [{ state: 'CA' }, { state: 'TX' }]
|
|
283
|
+
const variables: MarkupVariable[] = [
|
|
284
|
+
{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
const content = '{{state}}'
|
|
288
|
+
const result = processMarkupVariables(content, data, variables, { isEditor: true })
|
|
289
|
+
|
|
290
|
+
expect(result.processedContent).toBe('CA or TX')
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe('XSS Prevention and Security', () => {
|
|
295
|
+
it('should handle data with HTML tags safely', () => {
|
|
296
|
+
const maliciousData = [
|
|
297
|
+
{ value: '<script>alert("xss")</script>' },
|
|
298
|
+
{ value: '<img src=x onerror=alert(1)>' }
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
const variables: MarkupVariable[] = [
|
|
302
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
const content = 'Data: {{value}}'
|
|
306
|
+
const result = processMarkupVariables(content, maliciousData, variables)
|
|
307
|
+
|
|
308
|
+
// Should return the raw strings, parsing responsibility is on the component using html-react-parser
|
|
309
|
+
expect(result.processedContent).toContain('<script>')
|
|
310
|
+
expect(result.processedContent).toContain('<img')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('should handle special characters in data', () => {
|
|
314
|
+
const specialData = [
|
|
315
|
+
{ value: 'Test & Value' },
|
|
316
|
+
{ value: 'Price: $100 < $200' }
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
const variables: MarkupVariable[] = [
|
|
320
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
const content = '{{value}}'
|
|
324
|
+
const result = processMarkupVariables(content, specialData, variables)
|
|
325
|
+
|
|
326
|
+
expect(result.processedContent).toContain('Test & Value')
|
|
327
|
+
expect(result.processedContent).toContain('Price: $100 < $200')
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe('Hide Section Logic', () => {
|
|
332
|
+
it('should set shouldHideSection when allowHideSection is true and values are empty', () => {
|
|
333
|
+
const emptyData = [{ value: '' }]
|
|
334
|
+
const variables: MarkupVariable[] = [
|
|
335
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
const content = '{{value}}'
|
|
339
|
+
const result = processMarkupVariables(content, emptyData, variables, {
|
|
340
|
+
allowHideSection: true,
|
|
341
|
+
isEditor: false
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
expect(result.shouldHideSection).toBe(true)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('should not hide section in editor mode even if values are empty', () => {
|
|
348
|
+
const emptyData = [{ value: '' }]
|
|
349
|
+
const variables: MarkupVariable[] = [
|
|
350
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
const content = '{{value}}'
|
|
354
|
+
const result = processMarkupVariables(content, emptyData, variables, {
|
|
355
|
+
allowHideSection: true,
|
|
356
|
+
isEditor: true
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
expect(result.shouldHideSection).toBe(false)
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
describe('No Data Message Logic', () => {
|
|
364
|
+
it('should set shouldShowNoDataMessage when enabled and values are empty', () => {
|
|
365
|
+
const emptyData = [{ value: '' }]
|
|
366
|
+
const variables: MarkupVariable[] = [
|
|
367
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
const content = '{{value}}'
|
|
371
|
+
const result = processMarkupVariables(content, emptyData, variables, {
|
|
372
|
+
showNoDataMessage: true,
|
|
373
|
+
isEditor: false
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
expect(result.shouldShowNoDataMessage).toBe(true)
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
describe('Edge Cases', () => {
|
|
381
|
+
it('should handle empty data array', () => {
|
|
382
|
+
const variables: MarkupVariable[] = [
|
|
383
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
const content = '{{value}}'
|
|
387
|
+
const result = processMarkupVariables(content, [], variables)
|
|
388
|
+
|
|
389
|
+
expect(result.processedContent).toBe('')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('should handle empty content string', () => {
|
|
393
|
+
const variables: MarkupVariable[] = [
|
|
394
|
+
{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
const result = processMarkupVariables('', testData, variables)
|
|
398
|
+
|
|
399
|
+
expect(result.processedContent).toBe('')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('should handle single item (no conjunction)', () => {
|
|
403
|
+
const data = [{ state: 'California' }]
|
|
404
|
+
const variables: MarkupVariable[] = [
|
|
405
|
+
{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
const content = '{{state}}'
|
|
409
|
+
const result = processMarkupVariables(content, data, variables)
|
|
410
|
+
|
|
411
|
+
expect(result.processedContent).toBe('California')
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('should remove duplicate values from list', () => {
|
|
415
|
+
const duplicateData = [
|
|
416
|
+
{ state: 'California' },
|
|
417
|
+
{ state: 'Texas' },
|
|
418
|
+
{ state: 'California' }
|
|
419
|
+
]
|
|
420
|
+
|
|
421
|
+
const variables: MarkupVariable[] = [
|
|
422
|
+
{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
const content = '{{state}}'
|
|
426
|
+
const result = processMarkupVariables(content, duplicateData, variables)
|
|
427
|
+
|
|
428
|
+
expect(result.processedContent).toBe('California and Texas')
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
describe('validateMarkupVariables', () => {
|
|
434
|
+
const testData = [
|
|
435
|
+
{ state: 'CA', population: '1000' }
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
it('should return no errors for valid configuration', () => {
|
|
439
|
+
const variables: MarkupVariable[] = [
|
|
440
|
+
{
|
|
441
|
+
name: 'State',
|
|
442
|
+
tag: '{{state}}',
|
|
443
|
+
columnName: 'state',
|
|
444
|
+
conditions: []
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
const errors = validateMarkupVariables(variables, testData)
|
|
449
|
+
expect(errors).toHaveLength(0)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('should detect invalid tag format', () => {
|
|
453
|
+
const variables: MarkupVariable[] = [
|
|
454
|
+
{
|
|
455
|
+
name: 'State',
|
|
456
|
+
tag: 'invalid-tag',
|
|
457
|
+
columnName: 'state',
|
|
458
|
+
conditions: []
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
|
|
462
|
+
const errors = validateMarkupVariables(variables, testData)
|
|
463
|
+
expect(errors).toContain('Variable 1: Tag must be in format {{tagName}}')
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('should detect missing column name', () => {
|
|
467
|
+
const variables: MarkupVariable[] = [
|
|
468
|
+
{
|
|
469
|
+
name: 'State',
|
|
470
|
+
tag: '{{state}}',
|
|
471
|
+
columnName: '',
|
|
472
|
+
conditions: []
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
|
|
476
|
+
const errors = validateMarkupVariables(variables, testData)
|
|
477
|
+
expect(errors).toContain('Variable 1: Column name is required')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('should detect column not found in data', () => {
|
|
481
|
+
const variables: MarkupVariable[] = [
|
|
482
|
+
{
|
|
483
|
+
name: 'Invalid',
|
|
484
|
+
tag: '{{invalid}}',
|
|
485
|
+
columnName: 'nonexistent',
|
|
486
|
+
conditions: []
|
|
487
|
+
}
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
const errors = validateMarkupVariables(variables, testData)
|
|
491
|
+
expect(errors).toContain('Variable 1: Column "nonexistent" not found in data')
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
it('should detect invalid conditions', () => {
|
|
495
|
+
const variables: MarkupVariable[] = [
|
|
496
|
+
{
|
|
497
|
+
name: 'State',
|
|
498
|
+
tag: '{{state}}',
|
|
499
|
+
columnName: 'state',
|
|
500
|
+
conditions: [
|
|
501
|
+
{ columnName: '', isOrIsNotEqualTo: 'is', value: 'CA' },
|
|
502
|
+
{ columnName: 'badcolumn', isOrIsNotEqualTo: 'is', value: 'test' },
|
|
503
|
+
{ columnName: 'state', isOrIsNotEqualTo: 'is', value: '' }
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
]
|
|
507
|
+
|
|
508
|
+
const errors = validateMarkupVariables(variables, testData)
|
|
509
|
+
expect(errors).toContain('Variable 1, Condition 1: Column name is required')
|
|
510
|
+
expect(errors).toContain('Variable 1, Condition 2: Column "badcolumn" not found in data')
|
|
511
|
+
expect(errors).toContain('Variable 1, Condition 3: Value is required')
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
it('should handle empty data gracefully', () => {
|
|
515
|
+
const variables: MarkupVariable[] = [
|
|
516
|
+
{
|
|
517
|
+
name: 'State',
|
|
518
|
+
tag: '{{state}}',
|
|
519
|
+
columnName: 'state',
|
|
520
|
+
conditions: []
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
const errors = validateMarkupVariables(variables, [])
|
|
525
|
+
expect(errors).toHaveLength(0) // Should not error on column validation when data is empty
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it('should handle null or invalid input gracefully', () => {
|
|
529
|
+
const errors1 = validateMarkupVariables(null as any, testData)
|
|
530
|
+
expect(errors1).toHaveLength(0)
|
|
531
|
+
|
|
532
|
+
const errors2 = validateMarkupVariables(undefined as any, testData)
|
|
533
|
+
expect(errors2).toHaveLength(0)
|
|
534
|
+
|
|
535
|
+
const errors3 = validateMarkupVariables('not an array' as any, testData)
|
|
536
|
+
expect(errors3).toHaveLength(0)
|
|
537
|
+
})
|
|
538
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = path.dirname(__filename)
|
|
9
|
+
const packagesDir = path.join(__dirname, '..', 'packages')
|
|
10
|
+
|
|
11
|
+
function copyDirSync(src, dest) {
|
|
12
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
13
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
14
|
+
const srcPath = path.join(src, entry.name)
|
|
15
|
+
const destPath = path.join(dest, entry.name)
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
copyDirSync(srcPath, destPath)
|
|
18
|
+
} else {
|
|
19
|
+
fs.copyFileSync(srcPath, destPath)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function testStandaloneBuild(pkgDir) {
|
|
25
|
+
// This test can't be turned on until we've published the new version of @cdc/core
|
|
26
|
+
return true
|
|
27
|
+
|
|
28
|
+
pkgDir = pkgDir.replace('/src', '')
|
|
29
|
+
const pkgName = pkgDir.split('/')[pkgDir.split('/').length - 1]
|
|
30
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `cdc-open-viz-${pkgName}-`))
|
|
31
|
+
copyDirSync(pkgDir, tmpDir)
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
execSync('npm install', { cwd: tmpDir })
|
|
35
|
+
execSync('npm link @cdc/core', { cwd: tmpDir })
|
|
36
|
+
execSync('npm run build', { cwd: tmpDir })
|
|
37
|
+
return true
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`❌ Isolated build for ${pkgName} package failed`)
|
|
40
|
+
return false
|
|
41
|
+
} finally {
|
|
42
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { MarkupVariable } from '../types/MarkupVariable'
|
|
2
|
+
|
|
3
|
+
export type MarkupContext = {
|
|
4
|
+
markupVariables: MarkupVariable[]
|
|
5
|
+
enableMarkupVariables: boolean
|
|
6
|
+
isEditor: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// This will be passed as props to components that need markup highlighting
|
|
10
|
+
export const createMarkupContext = (
|
|
11
|
+
markupVariables: MarkupVariable[] = [],
|
|
12
|
+
enableMarkupVariables: boolean = false,
|
|
13
|
+
isEditor: boolean = false
|
|
14
|
+
): MarkupContext => ({
|
|
15
|
+
markupVariables,
|
|
16
|
+
enableMarkupVariables,
|
|
17
|
+
isEditor
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Helper to check if a markup variable tag is valid
|
|
21
|
+
export const isValidMarkupVariable = (
|
|
22
|
+
tag: string,
|
|
23
|
+
markupVariables: MarkupVariable[]
|
|
24
|
+
): boolean => {
|
|
25
|
+
return markupVariables.some(variable => variable.tag === tag)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Helper to get all valid markup variable tags
|
|
29
|
+
export const getValidMarkupTags = (markupVariables: MarkupVariable[]): string[] => {
|
|
30
|
+
return markupVariables.map(variable => variable.tag).filter(Boolean)
|
|
31
|
+
}
|
package/helpers/vegaConfig.ts
CHANGED
|
@@ -423,7 +423,6 @@ export const convertVegaConfig = (configType: string, vegaConfig: any, config: a
|
|
|
423
423
|
hideBorder: true,
|
|
424
424
|
title: colorLabel
|
|
425
425
|
}
|
|
426
|
-
config.color = 'sequential-blue-2(MPX)'
|
|
427
426
|
} else {
|
|
428
427
|
const stack = getStack(vegaConfig)
|
|
429
428
|
const stackField = stack?.field
|
package/helpers/ver/4.24.10.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
+
import cloneConfig from '../cloneConfig'
|
|
2
3
|
|
|
3
4
|
export const removeMultiSelectPropFromMultiselect = newConfig => {
|
|
4
5
|
if (newConfig.type === 'dashboard') {
|
|
@@ -47,7 +48,7 @@ export const defineFilterStyles = newConfig => {
|
|
|
47
48
|
|
|
48
49
|
const update_4_24_10 = config => {
|
|
49
50
|
const ver = '4.24.10'
|
|
50
|
-
const newConfig =
|
|
51
|
+
const newConfig = cloneConfig(config)
|
|
51
52
|
setXAxisLabelOffsetToZero(newConfig)
|
|
52
53
|
changePivotColumns(newConfig)
|
|
53
54
|
removeMultiSelectPropFromMultiselect(newConfig)
|
package/helpers/ver/4.24.11.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
+
import cloneConfig from '../cloneConfig'
|
|
2
3
|
|
|
3
4
|
const addColorMigration = config => {
|
|
4
5
|
// add new property
|
|
@@ -9,7 +10,7 @@ const addColorMigration = config => {
|
|
|
9
10
|
|
|
10
11
|
const update_4_24_11 = config => {
|
|
11
12
|
const ver = '4.24.11'
|
|
12
|
-
const newConfig =
|
|
13
|
+
const newConfig = cloneConfig(config)
|
|
13
14
|
addColorMigration(newConfig)
|
|
14
15
|
newConfig.version = ver
|
|
15
16
|
return newConfig
|
package/helpers/ver/4.24.3.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConfigRow } from '@cdc/dashboard/src/types/ConfigRow'
|
|
2
2
|
import _ from 'lodash'
|
|
3
|
+
import cloneConfig from '../cloneConfig'
|
|
3
4
|
|
|
4
5
|
const remapDashboardRows = config => {
|
|
5
6
|
if (config.type === 'dashboard') {
|
|
@@ -43,7 +44,7 @@ const mapUpdates = newConfig => {
|
|
|
43
44
|
const update_4_24_3 = config => {
|
|
44
45
|
const ver = '4.24.3'
|
|
45
46
|
|
|
46
|
-
const newConfig =
|
|
47
|
+
const newConfig = cloneConfig(config)
|
|
47
48
|
|
|
48
49
|
remapDashboardRows(newConfig)
|
|
49
50
|
chartUpdates(newConfig)
|
package/helpers/ver/4.24.4.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
+
import cloneConfig from '../cloneConfig'
|
|
2
3
|
|
|
3
4
|
const addFiltersToTables = config => {
|
|
4
5
|
if (config.type === 'dashboard') {
|
|
@@ -17,7 +18,7 @@ const addFiltersToTables = config => {
|
|
|
17
18
|
const update_4_24_4 = config => {
|
|
18
19
|
const ver = '4.24.4'
|
|
19
20
|
|
|
20
|
-
const newConfig =
|
|
21
|
+
const newConfig = cloneConfig(config)
|
|
21
22
|
addFiltersToTables(newConfig)
|
|
22
23
|
|
|
23
24
|
newConfig.version = ver
|
package/helpers/ver/4.24.5.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
+
import cloneConfig from '../cloneConfig'
|
|
2
3
|
|
|
3
4
|
const migrateMarkupInclude = newConfig => {
|
|
4
5
|
if (newConfig.type === 'markup-include') {
|
|
@@ -21,7 +22,7 @@ const migrateMarkupInclude = newConfig => {
|
|
|
21
22
|
const update_4_24_5 = config => {
|
|
22
23
|
const ver = '4.24.5'
|
|
23
24
|
|
|
24
|
-
const newConfig =
|
|
25
|
+
const newConfig = cloneConfig(config)
|
|
25
26
|
|
|
26
27
|
migrateMarkupInclude(newConfig)
|
|
27
28
|
|