@cdc/data-bite 4.25.11 → 4.26.1

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.
@@ -1,705 +0,0 @@
1
- import React, { memo, useContext, useEffect, useState } from 'react'
2
- import { DATA_FUNCTIONS, DATA_OPERATORS } from '@cdc/core/helpers/constants'
3
- import cloneConfig from '@cdc/core/helpers/cloneConfig'
4
- import _ from 'lodash'
5
- import {
6
- Accordion,
7
- AccordionItem,
8
- AccordionItemButton,
9
- AccordionItemHeading,
10
- AccordionItemPanel
11
- } from 'react-accessible-accordion'
12
-
13
- import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
14
- import Context from '../context'
15
- import WarningImage from '@cdc/core/assets/icon-warning-circle.svg'
16
- import Tooltip from '@cdc/core/components/ui/Tooltip'
17
- import Icon from '@cdc/core/components/ui/Icon'
18
- import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
19
- import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
20
- import { BITE_LOCATIONS, IMAGE_POSITIONS } from './../constants'
21
- import Layout from '@cdc/core/components/Layout'
22
- import { Select, TextField, CheckBox } from '@cdc/core/components/EditorPanel/Inputs'
23
- import Button from '@cdc/core/components/elements/Button'
24
- import PanelMarkup from '@cdc/core/components/EditorPanel/components/PanelMarkup'
25
- import { HeaderThemeSelector } from '@cdc/core/components/HeaderThemeSelector'
26
-
27
- import '@cdc/core/styles/v2/components/editor.scss'
28
-
29
- const EditorPanel = memo(() => {
30
- const { config, updateConfig, loading, data, setParentConfig, isDashboard, isEditor } = useContext(Context)
31
-
32
- const [displayPanel, setDisplayPanel] = useState(true)
33
-
34
- const updateField = updateFieldFactory(config, updateConfig, true)
35
-
36
- const missingRequiredSections = () => {
37
- //Whether to show error message if something is required to show a data-bite and isn't filled in
38
- return false
39
- }
40
-
41
- useEffect(() => {
42
- // Pass up to Editor if needed
43
- if (setParentConfig) {
44
- const newConfig = convertStateToConfig()
45
- delete newConfig.newViz
46
-
47
- setParentConfig(newConfig)
48
- }
49
- // eslint-disable-next-line react-hooks/exhaustive-deps
50
- }, [config])
51
-
52
- const onBackClick = () => {
53
- setDisplayPanel(!displayPanel)
54
- updateConfig({
55
- ...config,
56
- showEditorPanel: !displayPanel
57
- })
58
- }
59
-
60
- const convertStateToConfig = () => {
61
- let strippedState = cloneConfig(config)
62
- //if(false === missingRequiredSections()) {
63
- //strippedState.newViz
64
- //}
65
- delete strippedState.runtime
66
-
67
- return strippedState
68
- }
69
-
70
- // Filters -----------------------------------------------
71
- const removeFilter = index => {
72
- let filters = [...config.filters]
73
-
74
- filters.splice(index, 1)
75
-
76
- updateConfig({ ...config, filters })
77
- }
78
-
79
- const updateFilterProp = (name, index, value) => {
80
- let filters = [...config.filters]
81
-
82
- filters[index][name] = value
83
-
84
- updateConfig({ ...config, filters })
85
- }
86
-
87
- const addNewFilter = () => {
88
- let filters = config.filters ? [...config.filters] : []
89
-
90
- filters.push({ values: [] })
91
-
92
- updateConfig({ ...config, filters })
93
- }
94
-
95
- const getColumns = (filter = true) => {
96
- let columns = {}
97
- if (data.length) {
98
- data.map(row => {
99
- return Object.keys(row).forEach(columnName => (columns[columnName] = true))
100
- })
101
- }
102
-
103
- return Object.keys(columns)
104
- }
105
-
106
- const getFilterColumnValues = index => {
107
- let filterDataOptions = []
108
- const filterColumnName = config.filters[index].columnName
109
- if (data && filterColumnName) {
110
- data.forEach(function (row) {
111
- if (undefined !== row[filterColumnName] && -1 === filterDataOptions.indexOf(row[filterColumnName])) {
112
- filterDataOptions.push(row[filterColumnName])
113
- }
114
- })
115
- filterDataOptions.sort()
116
- }
117
- return filterDataOptions
118
- }
119
-
120
- // Dynamic Images ----------------------------------------
121
- const updateDynamicImage = (name, index, subindex = null, value) => {
122
- let imageOptions = [...config.imageData.options]
123
- null === subindex ? (imageOptions[index][name] = value) : (imageOptions[index].arguments[subindex][name] = value)
124
-
125
- let payload = { ...config.imageData, options: imageOptions }
126
- updateConfig({ ...config, imageData: payload })
127
- }
128
-
129
- const setDynamicArgument = (optionIndex, name, value) => {
130
- let imageArguments = [...config.imageData.options[optionIndex].arguments]
131
- imageArguments[1] = { ...imageArguments[1], [name]: value }
132
- let argumentsPayload = { ...config.imageData.options[optionIndex], arguments: imageArguments }
133
- let optionsPayload = [...config.imageData.options]
134
- optionsPayload[optionIndex] = argumentsPayload
135
- let payload = { ...config.imageData, options: optionsPayload }
136
- updateConfig({ ...config, imageData: payload })
137
- }
138
-
139
- const removeDynamicArgument = optionIndex => {
140
- if (config.imageData.options[optionIndex].arguments.length > 1) {
141
- let imageArguments = [...config.imageData.options[optionIndex].arguments]
142
- imageArguments.pop()
143
- let argumentsPayload = { ...config.imageData.options[optionIndex], arguments: imageArguments }
144
- let optionsPayload = [...config.imageData.options]
145
- optionsPayload[optionIndex] = argumentsPayload
146
- let payload = { ...config.imageData, options: optionsPayload }
147
- updateConfig({ ...config, imageData: payload })
148
- }
149
- }
150
-
151
- const addDynamicImage = () => {
152
- let imageOptions = config.imageData.options ? [...config.imageData.options] : []
153
- imageOptions.push({ source: '', arguments: [{ operator: '', threshold: '' }], alt: '', secondArgument: false })
154
-
155
- let payload = { ...config.imageData, options: imageOptions }
156
- updateConfig({ ...config, imageData: payload })
157
- }
158
-
159
- const removeDynamicImage = index => {
160
- let imageOptions = [...config.imageData.options]
161
- imageOptions.splice(index, 1)
162
-
163
- let payload = { ...config.imageData, options: imageOptions }
164
- updateConfig({ ...config, imageData: payload })
165
- }
166
-
167
- // General -----------------------------------------------
168
- if (loading) {
169
- return null
170
- }
171
-
172
- return (
173
- <ErrorBoundary component='EditorPanel'>
174
- <Layout.Sidebar
175
- isEditor={true}
176
- config={config}
177
- title='Configure Data Bites'
178
- onBackClick={onBackClick}
179
- displayPanel={displayPanel}
180
- >
181
- <section className='form-container'>
182
- <form>
183
- <Accordion allowZeroExpanded={true}>
184
- <AccordionItem>
185
- {' '}
186
- {/* General */}
187
- <AccordionItemHeading>
188
- <AccordionItemButton>General</AccordionItemButton>
189
- </AccordionItemHeading>
190
- <AccordionItemPanel>
191
- <Select
192
- value={config.biteStyle}
193
- fieldName='biteStyle'
194
- label='Data Bite Style'
195
- updateField={updateField}
196
- options={Object.entries(BITE_LOCATIONS).map(([key, value]) => ({ value: key, label: value }))}
197
- initial='Select'
198
- />
199
- <TextField
200
- value={config.title}
201
- fieldName='title'
202
- label='Title'
203
- placeholder='Data Bite Title'
204
- updateField={updateField}
205
- />
206
- <CheckBox
207
- value={config.visual?.showTitle}
208
- section='visual'
209
- checked={config.visual?.showTitle}
210
- fieldName='showTitle'
211
- label='Show Title'
212
- updateField={updateField}
213
- />
214
-
215
- <TextField
216
- type='textarea'
217
- value={config.biteBody}
218
- fieldName='biteBody'
219
- label='Message'
220
- updateField={updateField}
221
- tooltip={
222
- <Tooltip style={{ textTransform: 'none' }}>
223
- <Tooltip.Target>
224
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
225
- </Tooltip.Target>
226
- <Tooltip.Content>
227
- <p>
228
- Enter the message text for the visualization. The following HTML tags are supported: strong,
229
- em, sup, and sub.
230
- </p>
231
- </Tooltip.Content>
232
- </Tooltip>
233
- }
234
- />
235
- <TextField
236
- value={config.subtext}
237
- fieldName='subtext'
238
- label='Subtext/Citation'
239
- placeholder='Data Bite Subtext or Citation'
240
- updateField={updateField}
241
- tooltip={
242
- <Tooltip style={{ textTransform: 'none' }}>
243
- <Tooltip.Target>
244
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
245
- </Tooltip.Target>
246
- <Tooltip.Content>
247
- <p>
248
- Enter supporting text to display below the data visualization, if applicable. The following
249
- HTML tags are supported: strong, em, sup, and sub. You can also use markup variables like{' '}
250
- {'{{variable-name}}'} to display dynamic data.
251
- </p>
252
- </Tooltip.Content>
253
- </Tooltip>
254
- }
255
- />
256
- </AccordionItemPanel>
257
- </AccordionItem>
258
-
259
- <AccordionItem>
260
- {' '}
261
- {/*Data*/}
262
- <AccordionItemHeading>
263
- <AccordionItemButton>
264
- Data{' '}
265
- {(!config.dataColumn || !config.dataFunction) && (
266
- <WarningImage width='25' className='warning-icon' />
267
- )}
268
- </AccordionItemButton>
269
- </AccordionItemHeading>
270
- <AccordionItemPanel>
271
- <ul className='column-edit'>
272
- <li className='two-col'>
273
- <Select
274
- value={config.dataColumn || ''}
275
- fieldName='dataColumn'
276
- label='Data Column'
277
- updateField={updateField}
278
- initial='Select'
279
- required={true}
280
- options={getColumns() ? Array.from(getColumns()) : []}
281
- />
282
- <Select
283
- value={config.dataFunction || ''}
284
- fieldName='dataFunction'
285
- label='Data Function'
286
- updateField={updateField}
287
- initial='Select'
288
- required={true}
289
- options={
290
- Array.isArray(DATA_FUNCTIONS)
291
- ? DATA_FUNCTIONS
292
- : DATA_FUNCTIONS
293
- ? Object.values(DATA_FUNCTIONS)
294
- : []
295
- }
296
- />
297
- </li>
298
- </ul>
299
- <span className='divider-heading'>Number Formatting</span>
300
- <ul className='column-edit'>
301
- <li className='three-col'>
302
- <TextField
303
- value={config.dataFormat.prefix}
304
- section='dataFormat'
305
- fieldName='prefix'
306
- label='Prefix'
307
- updateField={updateField}
308
- />
309
- <TextField
310
- value={config.dataFormat.suffix}
311
- section='dataFormat'
312
- fieldName='suffix'
313
- label='Suffix'
314
- updateField={updateField}
315
- />
316
- <TextField
317
- type='number'
318
- value={config.dataFormat.roundToPlace}
319
- section='dataFormat'
320
- fieldName='roundToPlace'
321
- label='Round'
322
- updateField={updateField}
323
- min='0'
324
- max='99'
325
- />
326
- </li>
327
- </ul>
328
- <CheckBox
329
- value={config.dataFormat.commas}
330
- section='dataFormat'
331
- fieldName='commas'
332
- label='Add commas'
333
- updateField={updateField}
334
- />
335
- <CheckBox
336
- value={config.dataFormat.ignoreZeros}
337
- section='dataFormat'
338
- fieldName='ignoreZeros'
339
- label='Ignore Zeros'
340
- updateField={updateField}
341
- />
342
- <hr className='accordion__divider' />
343
-
344
- <label style={{ marginBottom: '1rem' }}>
345
- <span className='edit-label'>
346
- Data Point Filters
347
- <Tooltip style={{ textTransform: 'none' }}>
348
- <Tooltip.Target>
349
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
350
- </Tooltip.Target>
351
- <Tooltip.Content>
352
- <p>
353
- To refine the highlighted data point, specify one or more filters (e.g., "Male" and "Female"
354
- for a column called "Sex").
355
- </p>
356
- </Tooltip.Content>
357
- </Tooltip>
358
- </span>
359
- </label>
360
- {config.filters && (
361
- <ul className='filters-list'>
362
- {config.filters.map((filter, index) => (
363
- <fieldset className='edit-block' key={index}>
364
- <button
365
- type='button'
366
- className='btn btn-danger'
367
- onClick={() => {
368
- removeFilter(index)
369
- }}
370
- >
371
- Remove
372
- </button>
373
- <Select
374
- value={filter.columnName ? filter.columnName : ''}
375
- fieldName='columnName'
376
- label={'Column Name'}
377
- updateField={(section, subsection, fieldName, value) =>
378
- updateFilterProp(fieldName, index, value)
379
- }
380
- options={getColumns()}
381
- initial='- Select Option -'
382
- />
383
- <Select
384
- value={filter.columnValue || ''}
385
- fieldName='columnValue'
386
- label='Column Value'
387
- updateField={(section, subsection, fieldName, value) =>
388
- updateFilterProp(fieldName, index, value)
389
- }
390
- options={Array.isArray(getFilterColumnValues(index)) ? getFilterColumnValues(index) : []}
391
- initial='- Select Option -'
392
- />
393
- </fieldset>
394
- ))}
395
- </ul>
396
- )}
397
- {(!config.filters || config.filters.length === 0) && (
398
- <div>
399
- <fieldset className='edit-block'>
400
- <p style={{ textAlign: 'center' }}>There are currently no filters.</p>
401
- </fieldset>
402
- </div>
403
- )}
404
- <Button type='button' onClick={addNewFilter} className='btn btn-primary full-width mt-3'>
405
- Add Filter
406
- </Button>
407
- </AccordionItemPanel>
408
- </AccordionItem>
409
-
410
- <AccordionItem>
411
- {' '}
412
- {/*Visual*/}
413
- <AccordionItemHeading>
414
- <AccordionItemButton>Visual</AccordionItemButton>
415
- </AccordionItemHeading>
416
- <AccordionItemPanel className='panel-visual accordion__panel accordion__panel--visual'>
417
- <TextField
418
- type='number'
419
- value={config.biteFontSize}
420
- fieldName='biteFontSize'
421
- label='Bite Font Size'
422
- updateField={updateField}
423
- min='17'
424
- max='65'
425
- />
426
- <Select
427
- value={config.fontSize}
428
- fieldName='fontSize'
429
- label='Overall Font Size'
430
- updateField={updateField}
431
- options={['small', 'medium', 'large']}
432
- />
433
- <div className='checkbox-group'>
434
- <CheckBox
435
- value={config.visual?.border}
436
- section='visual'
437
- fieldName='border'
438
- label='Display Border'
439
- updateField={updateField}
440
- />
441
- <CheckBox
442
- value={config.visual?.borderColorTheme}
443
- section='visual'
444
- fieldName='borderColorTheme'
445
- label='Use Border Color Theme'
446
- updateField={updateField}
447
- />
448
- <CheckBox
449
- value={config.visual?.accent}
450
- section='visual'
451
- fieldName='accent'
452
- label='Use Accent Style'
453
- updateField={updateField}
454
- />
455
- <CheckBox
456
- value={config.visual?.background}
457
- section='visual'
458
- fieldName='background'
459
- label='Use Theme Background Color'
460
- updateField={updateField}
461
- />
462
- <CheckBox
463
- value={config.visual?.hideBackgroundColor}
464
- section='visual'
465
- fieldName='hideBackgroundColor'
466
- label='Hide Background Color'
467
- updateField={updateField}
468
- />
469
- </div>
470
- <HeaderThemeSelector
471
- selectedTheme={config.theme}
472
- onThemeSelect={theme => updateConfig({ ...config, theme })}
473
- label='Theme'
474
- />
475
- </AccordionItemPanel>
476
- </AccordionItem>
477
-
478
- {['title', 'body', 'graphic'].includes(config.biteStyle) && (
479
- <AccordionItem>
480
- {' '}
481
- {/*Image & Dynamic Images*/}
482
- <AccordionItemHeading>
483
- <AccordionItemButton>
484
- Image
485
- {['dynamic'].includes(config.imageData.display) && 's'}
486
- </AccordionItemButton>
487
- </AccordionItemHeading>
488
- <AccordionItemPanel>
489
- <Select
490
- value={config.imageData.display || ''}
491
- section='imageData'
492
- fieldName='display'
493
- label='Image Display Type'
494
- updateField={updateField}
495
- options={['none', 'static', 'dynamic']}
496
- />
497
- <Select
498
- value={config.bitePosition || ''}
499
- fieldName='bitePosition'
500
- label='Image/Graphic Position'
501
- updateField={updateField}
502
- initial='Select'
503
- options={IMAGE_POSITIONS}
504
- />
505
- {['static'].includes(config.imageData.display) && (
506
- <>
507
- <TextField
508
- value={config.imageData.url}
509
- section='imageData'
510
- fieldName='url'
511
- label='Image URL'
512
- updateField={updateField}
513
- />
514
- <TextField
515
- value={config.imageData.alt}
516
- section='imageData'
517
- fieldName='alt'
518
- label='Alt Text'
519
- updateField={updateField}
520
- />
521
- </>
522
- )}
523
-
524
- {['dynamic'].includes(config.imageData.display) && (
525
- <>
526
- <TextField
527
- value={config.imageData.url || ''}
528
- section='imageData'
529
- fieldName='url'
530
- label='Image URL (default)'
531
- updateField={updateField}
532
- />
533
- <TextField
534
- value={config.imageData.alt}
535
- section='imageData'
536
- fieldName='alt'
537
- label='Alt Text (default)'
538
- updateField={updateField}
539
- />
540
-
541
- <hr className='accordion__divider' />
542
-
543
- {(!config.imageData.options || config.imageData.options.length === 0) && (
544
- <p style={{ textAlign: 'center' }}>There are currently no dynamic images.</p>
545
- )}
546
- {config.imageData.options && config.imageData.options.length > 0 && (
547
- <>
548
- <ul>
549
- {config.imageData.options.map((option, index) => (
550
- <fieldset className='edit-block' key={index}>
551
- <button
552
- type='button'
553
- className='remove-column'
554
- onClick={() => {
555
- removeDynamicImage(index)
556
- }}
557
- >
558
- Remove
559
- </button>
560
- <label>
561
- <span className='edit-label column-heading'>
562
- <strong>{'Image #' + (index + 1)}</strong>
563
- </span>
564
-
565
- <div className='accordion__panel-row align-center'>
566
- <div className='accordion__panel-col flex-auto'>If Value</div>
567
- <div className='accordion__panel-col flex-auto'>
568
- <Select
569
- label=''
570
- value={option.arguments[0]?.operator || ''}
571
- options={DATA_OPERATORS}
572
- onChange={e => {
573
- updateDynamicImage('operator', index, 0, e.target.value)
574
- }}
575
- />
576
- </div>
577
- <div className='accordion__panel-col flex-grow flex-shrink'>
578
- <input
579
- type='number'
580
- value={option.arguments[0]?.threshold || ''}
581
- onChange={e => {
582
- updateDynamicImage('threshold', index, 0, e.target.value)
583
- }}
584
- />
585
- </div>
586
- </div>
587
-
588
- <div className='accordion__panel-row mb-2 align-center'>
589
- <div className='accordion__panel-col flex-grow'>
590
- <Select
591
- label=''
592
- value={option.secondArgument ? 'and' : 'then'}
593
- options={[
594
- { value: 'then', label: 'Then' },
595
- { value: 'and', label: 'And' }
596
- ]}
597
- onChange={e => {
598
- if ('then' === e.target.value) {
599
- updateDynamicImage('secondArgument', index, null, false)
600
- removeDynamicArgument(index)
601
- }
602
- if ('and' === e.target.value) {
603
- updateDynamicImage('secondArgument', index, null, true)
604
- }
605
- }}
606
- />
607
- </div>
608
- </div>
609
-
610
- {option.secondArgument && true === option.secondArgument && (
611
- <>
612
- <div className='accordion__panel-row align-center'>
613
- <div className='accordion__panel-col flex-auto'>If Value</div>
614
- <div className='accordion__panel-col flex-auto'>
615
- <Select
616
- label=''
617
- value={option.arguments[1]?.operator || ''}
618
- options={DATA_OPERATORS}
619
- onChange={e => {
620
- setDynamicArgument(index, 'operator', e.target.value)
621
- }}
622
- />
623
- </div>
624
- <div className='accordion__panel-col flex-grow flex-shrink'>
625
- <input
626
- type='number'
627
- value={option.arguments[1]?.threshold || ''}
628
- onChange={e => {
629
- setDynamicArgument(index, 'threshold', e.target.value)
630
- }}
631
- />
632
- </div>
633
- </div>
634
- <div className='accordion__panel-row mb-2 align-center text-center text-capitalize'>
635
- <div className='accordion__panel-col flex-grow'>Then</div>
636
- </div>
637
- </>
638
- )}
639
-
640
- <div className='accordion__panel-row mb-2 align-center'>
641
- <div className='accordion__panel-col flex-auto'>Show</div>
642
- <div className='accordion__panel-col flex-grow'>
643
- <input
644
- type='text'
645
- value={option.source || ''}
646
- onChange={e => {
647
- updateDynamicImage('source', index, null, e.target.value)
648
- }}
649
- />
650
- </div>
651
- </div>
652
-
653
- <div className='accordion__panel-row mb-2 align-center'>
654
- <div className='accordion__panel-col flex-auto'>Alt Text</div>
655
- <div className='accordion__panel-col flex-grow'>
656
- <input
657
- type='text'
658
- value={option.alt || ''}
659
- onChange={e => {
660
- updateDynamicImage('alt', index, null, e.target.value)
661
- }}
662
- />
663
- </div>
664
- </div>
665
- </label>
666
- </fieldset>
667
- ))}
668
- </ul>
669
- </>
670
- )}
671
- <button type='button' onClick={addDynamicImage} className='btn btn-primary full-width'>
672
- Add Dynamic Image
673
- </button>
674
- </>
675
- )}
676
- </AccordionItemPanel>
677
- </AccordionItem>
678
- )}
679
-
680
- <AccordionItem>
681
- <AccordionItemHeading>
682
- <AccordionItemButton>Markup Variables</AccordionItemButton>
683
- </AccordionItemHeading>
684
- <AccordionItemPanel>
685
- <PanelMarkup
686
- name='Markup Variables'
687
- markupVariables={config.markupVariables || []}
688
- data={data}
689
- enableMarkupVariables={config.enableMarkupVariables || false}
690
- onMarkupVariablesChange={variables => updateField(null, null, 'markupVariables', variables)}
691
- onToggleEnable={enabled => updateField(null, null, 'enableMarkupVariables', enabled)}
692
- withAccordion={false}
693
- />
694
- </AccordionItemPanel>
695
- </AccordionItem>
696
- <AdvancedEditor loadConfig={updateConfig} config={config} convertStateToConfig={convertStateToConfig} />
697
- </Accordion>
698
- </form>
699
- </section>
700
- </Layout.Sidebar>
701
- </ErrorBoundary>
702
- )
703
- })
704
-
705
- export default EditorPanel