@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.
Files changed (117) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +56 -38
  3. package/components/DataTable/components/ChartHeader.tsx +44 -14
  4. package/components/DataTable/components/ExpandCollapse.tsx +10 -1
  5. package/components/DataTable/components/MapHeader.tsx +24 -13
  6. package/components/DataTable/data-table.css +6 -0
  7. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  8. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  9. package/components/DownloadButton.tsx +40 -14
  10. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  11. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  12. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  13. package/components/ErrorBoundary.jsx +3 -1
  14. package/components/Filters/Filters.tsx +27 -20
  15. package/components/Filters/components/Tabs.tsx +1 -0
  16. package/components/Legend/Legend.Gradient.tsx +3 -6
  17. package/components/LegendShape.tsx +121 -3
  18. package/components/MediaControls.tsx +51 -3
  19. package/components/PaletteConversionModal.tsx +87 -0
  20. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  21. package/components/PaletteSelector/PaletteSelector.css +51 -0
  22. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  23. package/components/PaletteSelector/index.ts +2 -0
  24. package/components/RichTooltip/RichTooltip.tsx +1 -0
  25. package/components/Table/Table.tsx +3 -1
  26. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  27. package/components/_stories/DataTable.stories.tsx +1 -1
  28. package/components/_stories/Filters.stories.tsx +1 -1
  29. package/components/_stories/Footnotes.stories.tsx +1 -1
  30. package/components/_stories/Inputs.stories.tsx +1 -1
  31. package/components/_stories/MultiSelect.stories.tsx +3 -3
  32. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  33. package/components/_stories/Table.stories.tsx +1 -1
  34. package/components/elements/_stories/Button.stories.tsx +1 -1
  35. package/components/elements/_stories/Card.stories.tsx +1 -1
  36. package/components/inputs/InputToggle.tsx +2 -0
  37. package/components/managers/DataDesigner.tsx +10 -9
  38. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  39. package/components/ui/Tooltip.tsx +2 -1
  40. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  41. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  42. package/components/ui/_stories/Colors.stories.tsx +330 -0
  43. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  44. package/components/ui/_stories/Title.stories.tsx +1 -1
  45. package/contexts/EditorContext.ts +18 -0
  46. package/contexts/editor.actions.ts +28 -0
  47. package/contexts/editor.reducer.ts +94 -0
  48. package/data/chartColorPalettes.ts +118 -0
  49. package/data/colorPalettes.ts +9 -0
  50. package/data/mapColorPalettes.ts +45 -0
  51. package/data/sharedPalettes.ts +50 -0
  52. package/dist/cove-main.css +14 -11
  53. package/dist/cove-main.css.map +1 -1
  54. package/generateViteConfig.js +80 -0
  55. package/helpers/addValuesToFilters.ts +2 -3
  56. package/helpers/cloneConfig.ts +31 -0
  57. package/helpers/configDataHelpers.ts +128 -0
  58. package/helpers/configHelpers.ts +27 -0
  59. package/helpers/constants.ts +5 -2
  60. package/helpers/coveUpdateWorker.ts +13 -3
  61. package/helpers/filterColorPalettes.ts +152 -0
  62. package/helpers/generateColorsArray.ts +13 -0
  63. package/helpers/getColorPaletteVersion.ts +33 -0
  64. package/helpers/getPaletteAccessor.ts +18 -0
  65. package/helpers/markupProcessor.ts +205 -0
  66. package/helpers/metrics/helpers.ts +42 -19
  67. package/helpers/metrics/types.ts +48 -9
  68. package/helpers/metrics/utils.ts +34 -0
  69. package/helpers/palettes/colorDistributions.ts +56 -0
  70. package/helpers/palettes/migratePaletteName.ts +150 -0
  71. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  72. package/helpers/palettes/utils.ts +267 -0
  73. package/helpers/queryStringUtils.ts +13 -0
  74. package/helpers/testing.ts +345 -0
  75. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  76. package/helpers/tests/generateColorsArray.test.ts +24 -0
  77. package/helpers/tests/markupProcessor.test.ts +538 -0
  78. package/helpers/tests/testStandaloneBuild.ts +44 -0
  79. package/helpers/useMarkupVariables.ts +31 -0
  80. package/helpers/vegaConfig.ts +0 -1
  81. package/helpers/ver/4.24.10.ts +2 -1
  82. package/helpers/ver/4.24.11.ts +2 -1
  83. package/helpers/ver/4.24.3.ts +2 -1
  84. package/helpers/ver/4.24.4.ts +2 -1
  85. package/helpers/ver/4.24.5.ts +2 -1
  86. package/helpers/ver/4.24.7.ts +2 -1
  87. package/helpers/ver/4.24.9.ts +2 -1
  88. package/helpers/ver/4.25.1.ts +2 -1
  89. package/helpers/ver/4.25.10.ts +36 -0
  90. package/helpers/ver/4.25.3.ts +2 -1
  91. package/helpers/ver/4.25.4.ts +2 -1
  92. package/helpers/ver/4.25.6.ts +2 -1
  93. package/helpers/ver/4.25.7.ts +2 -1
  94. package/helpers/ver/4.25.8.ts +2 -1
  95. package/helpers/ver/4.25.9.ts +293 -0
  96. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  97. package/helpers/ver/tests/4.25.8.test.ts +1 -1
  98. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  99. package/hooks/useColorPalette.ts +79 -0
  100. package/package.json +12 -4
  101. package/styles/_global.scss +7 -5
  102. package/styles/base.scss +8 -5
  103. package/styles/v2/components/button.scss +4 -3
  104. package/styles/v2/components/editor.scss +2 -1
  105. package/styles/v2/layout/_data-table.scss +3 -2
  106. package/styles/v2/themes/_color-definitions.scss +18 -17
  107. package/testBuild.js +0 -0
  108. package/testing-setup.js +32 -0
  109. package/types/MarkupInclude.ts +6 -1
  110. package/types/MarkupVariable.ts +19 -0
  111. package/types/VizFilter.ts +1 -0
  112. package/vitest.config.ts +16 -0
  113. package/components/ui/_stories/Colors.stories.mdx +0 -220
  114. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  115. package/data/colorPalettes.js +0 -171
  116. package/helpers/formatConfigBeforeSave.ts +0 -135
  117. 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
+ }
@@ -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
@@ -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 = _.cloneDeep(config)
51
+ const newConfig = cloneConfig(config)
51
52
  setXAxisLabelOffsetToZero(newConfig)
52
53
  changePivotColumns(newConfig)
53
54
  removeMultiSelectPropFromMultiselect(newConfig)
@@ -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 = _.cloneDeep(config)
13
+ const newConfig = cloneConfig(config)
13
14
  addColorMigration(newConfig)
14
15
  newConfig.version = ver
15
16
  return newConfig
@@ -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 = _.cloneDeep(config)
47
+ const newConfig = cloneConfig(config)
47
48
 
48
49
  remapDashboardRows(newConfig)
49
50
  chartUpdates(newConfig)
@@ -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 = _.cloneDeep(config)
21
+ const newConfig = cloneConfig(config)
21
22
  addFiltersToTables(newConfig)
22
23
 
23
24
  newConfig.version = ver
@@ -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 = _.cloneDeep(config)
25
+ const newConfig = cloneConfig(config)
25
26
 
26
27
  migrateMarkupInclude(newConfig)
27
28