@cdc/markup-include 4.26.3 → 4.26.5
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/CONFIG.md +113 -0
- package/README.md +38 -14
- package/dist/cdcmarkupinclude.js +19331 -13297
- package/examples/minimal-example.json +11 -0
- package/package.json +4 -4
- package/src/CdcMarkupInclude.tsx +159 -40
- package/src/ConfigContext.ts +1 -0
- package/src/_stories/MarkupInclude.Editor.stories.tsx +46 -1
- package/src/_stories/MarkupInclude.smoke.stories.tsx +33 -0
- package/src/_stories/MarkupInclude.stories.tsx +121 -0
- package/src/cdcMarkupInclude.style.css +9 -6
- package/src/components/EditorPanel/EditorPanel.tsx +242 -47
- package/src/data/initial-state.js +7 -1
- package/src/index.tsx +6 -3
- package/src/scss/main.scss +64 -2
- package/src/test/CdcMarkupInclude.test.jsx +172 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useContext, useRef } from 'react'
|
|
1
|
+
import { useContext, useMemo, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
// Context
|
|
4
4
|
import ConfigContext from '../../ConfigContext'
|
|
5
5
|
|
|
6
6
|
// Helpers
|
|
7
7
|
import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
|
|
8
|
+
import { useDataColumns } from '@cdc/core/hooks/useDataColumns'
|
|
8
9
|
|
|
9
10
|
// Components
|
|
10
11
|
import { EditorPanel as BaseEditorPanel } from '@cdc/core/components/EditorPanel/EditorPanel'
|
|
@@ -15,7 +16,11 @@ import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
|
15
16
|
import MarkupVariablesEditor from '@cdc/core/components/EditorPanel/components/MarkupVariablesEditor'
|
|
16
17
|
import FootnotesEditor from '@cdc/core/components/EditorPanel/FootnotesEditor'
|
|
17
18
|
import StyleTreatmentSection from '@cdc/core/components/EditorPanel/sections/StyleTreatmentSection'
|
|
19
|
+
import { Tp5VisualSection } from '@cdc/core/components/EditorPanel/sections/Tp5VisualSection'
|
|
18
20
|
import { HeaderThemeSelector } from '@cdc/core/components/HeaderThemeSelector'
|
|
21
|
+
import { DataColorSelector } from '@cdc/core/components/DataColorSelector'
|
|
22
|
+
import { DATA_COLOR_PRESETS } from '@cdc/core/helpers/dataColors'
|
|
23
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
19
24
|
import { Datasets } from '@cdc/core/types/DataSet'
|
|
20
25
|
|
|
21
26
|
// styles
|
|
@@ -26,11 +31,106 @@ type MarkupIncludeEditorPanelProps = {
|
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
|
|
29
|
-
const { config, data, isDashboard, loading, setParentConfig, updateConfig } = useContext(ConfigContext)
|
|
34
|
+
const { config, data, editorData, isDashboard, loading, setParentConfig, updateConfig } = useContext(ConfigContext)
|
|
30
35
|
const { contentEditor, theme, visual } = config || {}
|
|
31
36
|
const { inlineHTML, srcUrl, title, useInlineHTML } = contentEditor || {}
|
|
37
|
+
const isTp5Style = contentEditor?.style === 'tp5'
|
|
32
38
|
const updateField = updateFieldFactory(config, updateConfig, true)
|
|
33
|
-
const styleTreatment = visual?.tp5Treatment ? 'tp5' : 'legacy'
|
|
39
|
+
const styleTreatment = (visual as any)?.tp5Treatment ? 'tp5' : 'legacy'
|
|
40
|
+
const markupEditorData = Array.isArray(editorData) ? editorData : data || []
|
|
41
|
+
const columns = useDataColumns(markupEditorData)
|
|
42
|
+
const dataColorMappings = config.dataColors?.mappings || []
|
|
43
|
+
|
|
44
|
+
const dataColorValues = useMemo(() => {
|
|
45
|
+
const colorColumn = config.dataColors?.column
|
|
46
|
+
if (!colorColumn) return []
|
|
47
|
+
|
|
48
|
+
const uniqueValues = new Set<string>()
|
|
49
|
+
markupEditorData?.forEach(row => {
|
|
50
|
+
const value = row?.[colorColumn]
|
|
51
|
+
if (value !== undefined && value !== null) {
|
|
52
|
+
uniqueValues.add(String(value))
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return Array.from(uniqueValues).sort()
|
|
57
|
+
}, [markupEditorData, config.dataColors?.column])
|
|
58
|
+
|
|
59
|
+
type DataColorDisplayEntry = { sourceValue: string; fromData: boolean; key: string }
|
|
60
|
+
|
|
61
|
+
const dataColorDisplayList = useMemo<DataColorDisplayEntry[]>(() => {
|
|
62
|
+
const dataSet = new Set(dataColorValues)
|
|
63
|
+
const list: DataColorDisplayEntry[] = dataColorValues.map(v => ({
|
|
64
|
+
sourceValue: v,
|
|
65
|
+
fromData: true,
|
|
66
|
+
key: `data-${v}`
|
|
67
|
+
}))
|
|
68
|
+
dataColorMappings.forEach((m, i) => {
|
|
69
|
+
if (!dataSet.has(m.sourceValue)) {
|
|
70
|
+
list.push({ sourceValue: m.sourceValue, fromData: false, key: `custom-${i}` })
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
return list
|
|
74
|
+
}, [dataColorValues, dataColorMappings])
|
|
75
|
+
|
|
76
|
+
const updateDataColorMapping = (sourceValue: string, color: string) => {
|
|
77
|
+
const nextMappings = [...dataColorMappings]
|
|
78
|
+
const existingIndex = nextMappings.findIndex(m => m.sourceValue === sourceValue)
|
|
79
|
+
|
|
80
|
+
if (!color) {
|
|
81
|
+
if (existingIndex > -1) {
|
|
82
|
+
nextMappings.splice(existingIndex, 1)
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const nextMapping = { sourceValue, color }
|
|
86
|
+
if (existingIndex > -1) {
|
|
87
|
+
nextMappings[existingIndex] = nextMapping
|
|
88
|
+
} else {
|
|
89
|
+
nextMappings.push(nextMapping)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
updateConfig({
|
|
94
|
+
...config,
|
|
95
|
+
dataColors: {
|
|
96
|
+
...config.dataColors,
|
|
97
|
+
mappings: nextMappings
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const addCustomDataColorMapping = () => {
|
|
103
|
+
const nextMappings = [...dataColorMappings, { sourceValue: '', color: DATA_COLOR_PRESETS[0] }]
|
|
104
|
+
updateConfig({
|
|
105
|
+
...config,
|
|
106
|
+
dataColors: {
|
|
107
|
+
...config.dataColors,
|
|
108
|
+
mappings: nextMappings
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const updateDataColorMappingValue = (oldValue: string, newValue: string) => {
|
|
114
|
+
const nextMappings = dataColorMappings.map(m => (m.sourceValue === oldValue ? { ...m, sourceValue: newValue } : m))
|
|
115
|
+
updateConfig({
|
|
116
|
+
...config,
|
|
117
|
+
dataColors: {
|
|
118
|
+
...config.dataColors,
|
|
119
|
+
mappings: nextMappings
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const removeDataColorMapping = (sourceValue: string) => {
|
|
125
|
+
const nextMappings = dataColorMappings.filter(m => m.sourceValue !== sourceValue)
|
|
126
|
+
updateConfig({
|
|
127
|
+
...config,
|
|
128
|
+
dataColors: {
|
|
129
|
+
...config.dataColors,
|
|
130
|
+
mappings: nextMappings
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
}
|
|
34
134
|
|
|
35
135
|
const handleStyleTreatmentChange = (value: string) => {
|
|
36
136
|
const useTp5Treatment = value === 'tp5'
|
|
@@ -74,6 +174,17 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
|
|
|
74
174
|
{() => (
|
|
75
175
|
<Accordion>
|
|
76
176
|
<Accordion.Section title='General'>
|
|
177
|
+
<Select
|
|
178
|
+
value={contentEditor?.style || 'default'}
|
|
179
|
+
section='contentEditor'
|
|
180
|
+
fieldName='style'
|
|
181
|
+
label='Style'
|
|
182
|
+
updateField={updateField}
|
|
183
|
+
options={[
|
|
184
|
+
{ value: 'default', label: 'Default' },
|
|
185
|
+
{ value: 'tp5', label: 'TP5' }
|
|
186
|
+
]}
|
|
187
|
+
/>
|
|
77
188
|
<TextField
|
|
78
189
|
value={title || ''}
|
|
79
190
|
section='contentEditor'
|
|
@@ -82,32 +193,34 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
|
|
|
82
193
|
placeholder='Markup Include Title'
|
|
83
194
|
updateField={updateField}
|
|
84
195
|
/>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<Tooltip
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
196
|
+
{!isTp5Style && (
|
|
197
|
+
<Select
|
|
198
|
+
value={contentEditor?.titleStyle || 'small'}
|
|
199
|
+
section='contentEditor'
|
|
200
|
+
fieldName='titleStyle'
|
|
201
|
+
label='Title Style'
|
|
202
|
+
updateField={updateField}
|
|
203
|
+
options={[
|
|
204
|
+
{ value: 'small', label: 'Small (h3)' },
|
|
205
|
+
{ value: 'large', label: 'Large (h2)' },
|
|
206
|
+
{ value: 'legacy', label: 'Legacy' }
|
|
207
|
+
]}
|
|
208
|
+
tooltip={
|
|
209
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
210
|
+
<Tooltip.Target>
|
|
211
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
212
|
+
</Tooltip.Target>
|
|
213
|
+
<Tooltip.Content>
|
|
214
|
+
<p>Choose the visual style for the title.</p>
|
|
215
|
+
<p>
|
|
216
|
+
Consider heading order on your page when selecting the title style. For 508 reasons, ensure your
|
|
217
|
+
page follows a proper heading order.
|
|
218
|
+
</p>
|
|
219
|
+
</Tooltip.Content>
|
|
220
|
+
</Tooltip>
|
|
221
|
+
}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
111
224
|
<Select
|
|
112
225
|
value={config.locale}
|
|
113
226
|
fieldName='locale'
|
|
@@ -170,24 +283,105 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
|
|
|
170
283
|
</div>
|
|
171
284
|
</Accordion.Section>
|
|
172
285
|
<Accordion.Section title='Visual'>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
286
|
+
{!isTp5Style && (
|
|
287
|
+
<HeaderThemeSelector
|
|
288
|
+
selectedTheme={config.theme}
|
|
289
|
+
onThemeSelect={theme => updateConfig({ ...config, theme })}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
{isTp5Style ? (
|
|
293
|
+
<Tp5VisualSection config={config} updateField={updateField} />
|
|
294
|
+
) : (
|
|
295
|
+
<StyleTreatmentSection
|
|
296
|
+
styleTreatment={styleTreatment}
|
|
297
|
+
onStyleTreatmentChange={handleStyleTreatmentChange}
|
|
298
|
+
showStyleTreatment={false}
|
|
299
|
+
border={config.visual?.border}
|
|
300
|
+
borderColorTheme={config.visual?.borderColorTheme}
|
|
301
|
+
accent={config.visual?.accent}
|
|
302
|
+
background={config.visual?.background}
|
|
303
|
+
hideBackgroundColor={config.visual?.hideBackgroundColor}
|
|
304
|
+
showBackground
|
|
305
|
+
showHideBackgroundColor
|
|
306
|
+
updateField={updateField}
|
|
307
|
+
/>
|
|
308
|
+
)}
|
|
190
309
|
</Accordion.Section>
|
|
310
|
+
{isTp5Style && (
|
|
311
|
+
<Accordion.Section title='Data-Driven Colors'>
|
|
312
|
+
<Select
|
|
313
|
+
value={config.dataColors?.column || ''}
|
|
314
|
+
section='dataColors'
|
|
315
|
+
fieldName='column'
|
|
316
|
+
label='Color Column'
|
|
317
|
+
updateField={updateField}
|
|
318
|
+
initial='Select'
|
|
319
|
+
options={columns}
|
|
320
|
+
tooltip={
|
|
321
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
322
|
+
<Tooltip.Target>
|
|
323
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
324
|
+
</Tooltip.Target>
|
|
325
|
+
<Tooltip.Content>
|
|
326
|
+
<p>
|
|
327
|
+
Choose a column whose values determine the background color of this visualization. Map each
|
|
328
|
+
value to a color below. Text color adjusts automatically for contrast.
|
|
329
|
+
</p>
|
|
330
|
+
</Tooltip.Content>
|
|
331
|
+
</Tooltip>
|
|
332
|
+
}
|
|
333
|
+
/>
|
|
334
|
+
{config.dataColors?.column && dataColorDisplayList.length > 0 && (
|
|
335
|
+
<div className='mt-2'>
|
|
336
|
+
{dataColorDisplayList.map(({ sourceValue, fromData, key }) => {
|
|
337
|
+
const selectedColor = dataColorMappings.find(m => m.sourceValue === sourceValue)?.color || ''
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<div className='cove-accordion__panel-row align-center mb-2' key={key}>
|
|
341
|
+
<div className='cove-accordion__panel-col' style={{ flex: '1 1 0', minWidth: 0 }}>
|
|
342
|
+
{fromData ? (
|
|
343
|
+
sourceValue
|
|
344
|
+
) : (
|
|
345
|
+
<input
|
|
346
|
+
type='text'
|
|
347
|
+
value={sourceValue}
|
|
348
|
+
placeholder='Enter value'
|
|
349
|
+
style={{ width: '100%' }}
|
|
350
|
+
onChange={e => updateDataColorMappingValue(sourceValue, e.target.value)}
|
|
351
|
+
/>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
<div className='cove-accordion__panel-col' style={{ flex: '0 0 4.5rem' }}>
|
|
355
|
+
<DataColorSelector
|
|
356
|
+
value={selectedColor}
|
|
357
|
+
onChange={color => updateDataColorMapping(sourceValue, color)}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
<div className='cove-accordion__panel-col' style={{ flex: '0 0 1.5rem' }}>
|
|
361
|
+
{!fromData && (
|
|
362
|
+
<button
|
|
363
|
+
type='button'
|
|
364
|
+
className='btn btn-danger'
|
|
365
|
+
style={{ padding: '0.15rem 0.45rem', lineHeight: 1 }}
|
|
366
|
+
title='Remove mapping'
|
|
367
|
+
onClick={() => removeDataColorMapping(sourceValue)}
|
|
368
|
+
>
|
|
369
|
+
−
|
|
370
|
+
</button>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)
|
|
375
|
+
})}
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
{config.dataColors?.column && (
|
|
379
|
+
<Button type='button' onClick={addCustomDataColorMapping} className='btn btn-primary full-width mt-3'>
|
|
380
|
+
Add Color Mapping
|
|
381
|
+
</Button>
|
|
382
|
+
)}
|
|
383
|
+
</Accordion.Section>
|
|
384
|
+
)}
|
|
191
385
|
{isDashboard && (
|
|
192
386
|
<Accordion.Section title='Footnotes'>
|
|
193
387
|
<FootnotesEditor config={config} updateField={updateField} datasets={datasets} />
|
|
@@ -196,7 +390,8 @@ const EditorPanel: React.FC<MarkupIncludeEditorPanelProps> = ({ datasets }) => {
|
|
|
196
390
|
<Accordion.Section title='Markup Variables'>
|
|
197
391
|
<MarkupVariablesEditor
|
|
198
392
|
markupVariables={config.markupVariables || []}
|
|
199
|
-
data={data
|
|
393
|
+
data={Array.isArray(data) ? data : []}
|
|
394
|
+
editorData={markupEditorData}
|
|
200
395
|
datasets={datasets}
|
|
201
396
|
config={config}
|
|
202
397
|
onChange={handleMarkupVariablesChange}
|
|
@@ -2,6 +2,7 @@ export default {
|
|
|
2
2
|
contentEditor: {
|
|
3
3
|
inlineHTML: '<strong>Inline HTML</strong>',
|
|
4
4
|
showHeader: true,
|
|
5
|
+
style: 'default',
|
|
5
6
|
srcUrl: '#example',
|
|
6
7
|
title: '',
|
|
7
8
|
titleStyle: 'small',
|
|
@@ -22,7 +23,12 @@ export default {
|
|
|
22
23
|
accent: false,
|
|
23
24
|
background: false,
|
|
24
25
|
hideBackgroundColor: false,
|
|
25
|
-
borderColorTheme: false
|
|
26
|
+
borderColorTheme: false,
|
|
27
|
+
whiteBackground: false
|
|
28
|
+
},
|
|
29
|
+
dataColors: {
|
|
30
|
+
column: '',
|
|
31
|
+
mappings: []
|
|
26
32
|
},
|
|
27
33
|
markupVariables: [],
|
|
28
34
|
enableMarkupVariables: false
|
package/src/index.tsx
CHANGED
|
@@ -9,14 +9,17 @@ import CdcMarkupInclude from './CdcMarkupInclude'
|
|
|
9
9
|
|
|
10
10
|
let isEditor = window.location.href.includes('editor=true')
|
|
11
11
|
|
|
12
|
-
let domContainer = document.getElementsByClassName('react-container')[0]
|
|
12
|
+
let domContainer = document.getElementsByClassName('react-container')[0] as HTMLElement & { coveConfig?: any }
|
|
13
|
+
let configUrl = domContainer.dataset.configUrl
|
|
14
|
+
let injectedConfig = domContainer.coveConfig
|
|
13
15
|
|
|
14
16
|
ReactDOM.createRoot(domContainer).render(
|
|
15
17
|
<React.StrictMode>
|
|
16
18
|
<GlobalContextProvider>
|
|
17
19
|
<CdcMarkupInclude
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
config={injectedConfig}
|
|
21
|
+
configUrl={injectedConfig ? undefined : configUrl}
|
|
22
|
+
interactionLabel={configUrl}
|
|
20
23
|
isEditor={isEditor}
|
|
21
24
|
/>
|
|
22
25
|
</GlobalContextProvider>
|
package/src/scss/main.scss
CHANGED
|
@@ -1,10 +1,72 @@
|
|
|
1
1
|
@import '@cdc/core/styles/layout/wrapper-padding';
|
|
2
|
+
@import '@cdc/core/styles/layout/callout';
|
|
2
3
|
|
|
3
4
|
.cove-visualization.type-markup-include {
|
|
4
5
|
.cove-visualization__body {
|
|
5
6
|
@include cove-visualization-body-padding;
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
.markup-include-component--tp5 {
|
|
10
|
+
padding: 0 !important;
|
|
11
|
+
border: none !important;
|
|
12
|
+
background: none !important;
|
|
13
|
+
container-type: inline-size;
|
|
14
|
+
|
|
15
|
+
.markup-include-tp5 {
|
|
16
|
+
gap: 0.7rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.cdc-callout__heading {
|
|
20
|
+
width: 100%;
|
|
21
|
+
font-size: 1.1rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.cdc-callout__body {
|
|
25
|
+
flex-wrap: nowrap;
|
|
26
|
+
flex: auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@container (max-width: 576px) {
|
|
30
|
+
.cdc-callout__body {
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.cdc-callout__content {
|
|
36
|
+
font-size: 1rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.cdc-callout--data-color {
|
|
40
|
+
.cdc-callout__heading,
|
|
41
|
+
.cdc-callout__content {
|
|
42
|
+
color: inherit;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&.white-background-style {
|
|
47
|
+
.markup-include-tp5 {
|
|
48
|
+
border: 1px solid #009ec1 !important;
|
|
49
|
+
background: transparent;
|
|
50
|
+
box-shadow: 0 2px 4px rgb(159 159 159 / 10%);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.cdc-callout--data-color {
|
|
54
|
+
border: none !important;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&.white-background-style.display-border {
|
|
59
|
+
.markup-include-tp5 {
|
|
60
|
+
box-shadow: 0 2px 4px rgb(159 159 159 / 10%);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.cove-visualization__body-wrap,
|
|
65
|
+
.cove-visualization__content-section {
|
|
66
|
+
padding: 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
8
70
|
.cove-visualization__body-wrap {
|
|
9
71
|
@include cove-visualization-body-wrap-inline-padding;
|
|
10
72
|
|
|
@@ -22,7 +84,7 @@
|
|
|
22
84
|
// keep markup include images inlines on dashboards
|
|
23
85
|
// e.g. requested to help here: https://www.cdc.gov/fluview/surveillance/2026-week-10.html
|
|
24
86
|
// needed for backwards compatibility with existing dashboards, but also to prevent any future issues with images in markup include visualizations being forced to display as blocks
|
|
25
|
-
span>img {
|
|
87
|
+
span > img {
|
|
26
88
|
display: inline;
|
|
27
89
|
}
|
|
28
90
|
}
|
|
@@ -54,4 +116,4 @@
|
|
|
54
116
|
.cove-editor .cove-editor__content {
|
|
55
117
|
padding-left: 350px;
|
|
56
118
|
display: flex;
|
|
57
|
-
}
|
|
119
|
+
}
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import vm from 'node:vm'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
2
6
|
import { testStandaloneBuild } from '@cdc/core/helpers/tests/testStandaloneBuild.ts'
|
|
3
|
-
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
8
|
+
import CdcMarkupInclude from '../CdcMarkupInclude'
|
|
9
|
+
|
|
10
|
+
vi.mock('@cdc/core/components/EditorPanel/components/MarkupVariablesEditor', () => ({
|
|
11
|
+
default: ({ data, editorData }) => (
|
|
12
|
+
<div>
|
|
13
|
+
<div data-testid='markup-variables-editor-data'>{JSON.stringify(data)}</div>
|
|
14
|
+
<div data-testid='markup-variables-editor-editor-data'>{JSON.stringify(editorData)}</div>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
const extractMarkedExampleConfig = (content, label) => {
|
|
20
|
+
const match = content.match(
|
|
21
|
+
/<!-- README_EXAMPLE_CONFIG_START -->\s*```jsx\s*([\s\S]*?)\s*```\s*<!-- README_EXAMPLE_CONFIG_END -->/
|
|
22
|
+
)
|
|
23
|
+
expect(match, `${label} should contain a marked README example block`).toBeTruthy()
|
|
24
|
+
const configMatch = match[1].match(/const config = (\{[\s\S]*?\})\n\nfunction App\(\)/)
|
|
25
|
+
expect(configMatch, `${label} should define const config before function App()`).toBeTruthy()
|
|
26
|
+
return vm.runInNewContext(`(${configMatch[1]})`)
|
|
27
|
+
}
|
|
4
28
|
|
|
5
29
|
describe('Markup Include', () => {
|
|
6
30
|
it('Can be built in isolation', async () => {
|
|
@@ -8,4 +32,151 @@ describe('Markup Include', () => {
|
|
|
8
32
|
const result = await testStandaloneBuild(pkgDir)
|
|
9
33
|
expect(result).toBe(true)
|
|
10
34
|
}, 300000)
|
|
35
|
+
|
|
36
|
+
it('uses dashboard rawData for markup variable editor choices while preserving render data when editing', async () => {
|
|
37
|
+
const filteredData = [{ category: 'Filtered only' }]
|
|
38
|
+
const fullData = [{ category: 'Value A' }, { category: 'Value B' }]
|
|
39
|
+
|
|
40
|
+
render(
|
|
41
|
+
<CdcMarkupInclude
|
|
42
|
+
config={{
|
|
43
|
+
type: 'markup-include',
|
|
44
|
+
theme: 'theme-blue',
|
|
45
|
+
dataKey: 'test-dataset',
|
|
46
|
+
data: filteredData,
|
|
47
|
+
markupVariables: [],
|
|
48
|
+
contentEditor: {
|
|
49
|
+
title: '',
|
|
50
|
+
inlineHTML: '<p>Example</p>',
|
|
51
|
+
useInlineHTML: true,
|
|
52
|
+
srcUrl: ''
|
|
53
|
+
},
|
|
54
|
+
visual: {
|
|
55
|
+
border: false,
|
|
56
|
+
accent: false,
|
|
57
|
+
background: false,
|
|
58
|
+
hideBackgroundColor: false,
|
|
59
|
+
borderColorTheme: false
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
datasets={{
|
|
63
|
+
'test-dataset': {
|
|
64
|
+
data: [{ category: 'Dataset fallback' }]
|
|
65
|
+
}
|
|
66
|
+
}}
|
|
67
|
+
rawData={fullData}
|
|
68
|
+
isDashboard={true}
|
|
69
|
+
isEditor={true}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
expect(JSON.parse((await screen.findByTestId('markup-variables-editor-data')).textContent)).toEqual(filteredData)
|
|
74
|
+
expect(JSON.parse((await screen.findByTestId('markup-variables-editor-editor-data')).textContent)).toEqual(fullData)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('does not render the visualization filters section when every markup include filter is hidden', async () => {
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<CdcMarkupInclude
|
|
80
|
+
config={{
|
|
81
|
+
type: 'markup-include',
|
|
82
|
+
theme: 'theme-blue',
|
|
83
|
+
data: [{ state: 'Alabama' }],
|
|
84
|
+
filters: [
|
|
85
|
+
{
|
|
86
|
+
columnName: 'state',
|
|
87
|
+
values: ['Alabama'],
|
|
88
|
+
showDropdown: false,
|
|
89
|
+
id: 1,
|
|
90
|
+
parents: [],
|
|
91
|
+
active: 'Alabama',
|
|
92
|
+
filterStyle: 'dropdown',
|
|
93
|
+
label: 'State',
|
|
94
|
+
order: 'asc',
|
|
95
|
+
type: 'url'
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
markupVariables: [],
|
|
99
|
+
contentEditor: {
|
|
100
|
+
title: '',
|
|
101
|
+
inlineHTML: '<p>Example</p>',
|
|
102
|
+
useInlineHTML: true,
|
|
103
|
+
srcUrl: ''
|
|
104
|
+
},
|
|
105
|
+
visual: {
|
|
106
|
+
border: false,
|
|
107
|
+
accent: false,
|
|
108
|
+
background: false,
|
|
109
|
+
hideBackgroundColor: false,
|
|
110
|
+
borderColorTheme: false
|
|
111
|
+
}
|
|
112
|
+
}}
|
|
113
|
+
datasets={{}}
|
|
114
|
+
isDashboard={true}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
await waitFor(() => expect(container.querySelector('.markup-include-content-container')).toBeInTheDocument())
|
|
119
|
+
|
|
120
|
+
expect(container.querySelector('.cove-visualization__filters-section')).not.toBeInTheDocument()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('keeps included bootstrap column markup inside the shared content section', async () => {
|
|
124
|
+
const { container } = render(
|
|
125
|
+
<CdcMarkupInclude
|
|
126
|
+
config={{
|
|
127
|
+
type: 'markup-include',
|
|
128
|
+
theme: 'theme-blue',
|
|
129
|
+
markupVariables: [],
|
|
130
|
+
contentEditor: {
|
|
131
|
+
title: 'Grid mock',
|
|
132
|
+
inlineHTML: `
|
|
133
|
+
<div class="row">
|
|
134
|
+
<div class="col-8"><p>Primary content</p></div>
|
|
135
|
+
<div class="col-3"><p>Secondary content</p></div>
|
|
136
|
+
</div>
|
|
137
|
+
`,
|
|
138
|
+
useInlineHTML: true,
|
|
139
|
+
srcUrl: ''
|
|
140
|
+
},
|
|
141
|
+
visual: {
|
|
142
|
+
border: false,
|
|
143
|
+
accent: false,
|
|
144
|
+
background: false,
|
|
145
|
+
hideBackgroundColor: false,
|
|
146
|
+
borderColorTheme: false
|
|
147
|
+
}
|
|
148
|
+
}}
|
|
149
|
+
datasets={{}}
|
|
150
|
+
isDashboard={true}
|
|
151
|
+
/>
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
await waitFor(() => expect(container.querySelector('.markup-include-content-container')).toBeInTheDocument())
|
|
155
|
+
|
|
156
|
+
const shell = container.querySelector('.cove-visualization.type-markup-include')
|
|
157
|
+
const contentSection = container.querySelector('.cove-visualization__content-section')
|
|
158
|
+
const primaryColumn = container.querySelector('.col-8')
|
|
159
|
+
const secondaryColumn = container.querySelector('.col-3')
|
|
160
|
+
|
|
161
|
+
expect(shell).toBeInTheDocument()
|
|
162
|
+
expect(contentSection).toBeInTheDocument()
|
|
163
|
+
expect(primaryColumn).toBeInTheDocument()
|
|
164
|
+
expect(secondaryColumn).toBeInTheDocument()
|
|
165
|
+
expect(primaryColumn?.closest('.cove-visualization__content-section')).toBe(contentSection)
|
|
166
|
+
expect(secondaryColumn?.closest('.cove-visualization__content-section')).toBe(contentSection)
|
|
167
|
+
expect(primaryColumn?.parentElement).not.toBe(shell)
|
|
168
|
+
expect(secondaryColumn?.parentElement).not.toBe(shell)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('keeps the minimal example in sync with the README docs', () => {
|
|
172
|
+
const pkgRoot = path.join(__dirname, '..', '..')
|
|
173
|
+
const minimalExamplePath = path.join(pkgRoot, 'examples', 'minimal-example.json')
|
|
174
|
+
const readmePath = path.join(pkgRoot, 'README.md')
|
|
175
|
+
|
|
176
|
+
const minimalExample = JSON.parse(fs.readFileSync(minimalExamplePath, 'utf8'))
|
|
177
|
+
const readmeBlock = extractMarkedExampleConfig(fs.readFileSync(readmePath, 'utf8'), 'README.md')
|
|
178
|
+
|
|
179
|
+
expect(readmeBlock).toEqual(minimalExample)
|
|
180
|
+
expect(minimalExample.version).toBeTruthy()
|
|
181
|
+
})
|
|
11
182
|
})
|