@cdc/data-bite 1.1.1 → 1.1.3

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.
@@ -0,0 +1,590 @@
1
+ import React, { memo, useContext, useEffect, useState } from 'react'
2
+
3
+ import {
4
+ Accordion,
5
+ AccordionItem,
6
+ AccordionItemButton,
7
+ AccordionItemHeading,
8
+ AccordionItemPanel,
9
+ } from 'react-accessible-accordion'
10
+
11
+ import { useDebounce } from 'use-debounce'
12
+ import Context from '../context'
13
+ import WarningImage from '../images/warning.svg'
14
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
15
+ import Icon from '@cdc/core/components/ui/Icon'
16
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
17
+ import { BITE_LOCATIONS, DATA_FUNCTIONS, IMAGE_POSITIONS, DATA_OPERATORS } from '../CdcDataBite'
18
+
19
+ const TextField = memo(({label, section = null, subsection = null, fieldName, updateField, value: stateValue, tooltip, type = "input", i = null, min = null, max = null, ...attributes}) => {
20
+ const [ value, setValue ] = useState(stateValue);
21
+
22
+ const [ debouncedValue ] = useDebounce(value, 500);
23
+
24
+ useEffect(() => {
25
+ if('string' === typeof debouncedValue && stateValue !== debouncedValue ) {
26
+ updateField(section, subsection, fieldName, debouncedValue, i)
27
+ }
28
+ }, [debouncedValue, section, subsection, fieldName, i, stateValue, updateField])
29
+
30
+ let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`;
31
+
32
+ const onChange = (e) => {
33
+ //TODO: This block gives a warning/error in the console, but it still works.
34
+ if('number' !== type || min === null){
35
+ setValue(e.target.value);
36
+ } else {
37
+ if(!e.target.value || ( parseFloat(min) <= parseFloat(e.target.value ) & parseFloat(max) >= parseFloat(e.target.value))) {
38
+ setValue(e.target.value);
39
+ } else {
40
+ setValue(min.toString());
41
+ }
42
+ }
43
+ };
44
+
45
+ let formElement = <input type="text" name={name} onChange={onChange} {...attributes} value={value} />
46
+
47
+ if('textarea' === type) {
48
+ formElement = (
49
+ <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
50
+ )
51
+ }
52
+
53
+ if('number' === type) {
54
+ formElement = <input type="number" name={name} onChange={onChange} {...attributes} value={value} />
55
+ }
56
+
57
+ return (
58
+ <>
59
+ {label && label.length > 0 &&
60
+ <label>
61
+ <span className="edit-label column-heading">{label}{tooltip}</span>
62
+ {formElement}
63
+ </label>
64
+ }
65
+ {(!label || label.length === 0) && formElement}
66
+ </>
67
+ )
68
+ })
69
+
70
+ const CheckBox = memo(({label, value, fieldName, section = null, subsection = null, tooltip, updateField, ...attributes}) => (
71
+ <label className="checkbox">
72
+ <input type="checkbox" name={fieldName} checked={ value } onChange={() => { updateField(section, subsection, fieldName, !value) }} {...attributes}/>
73
+ <span className="edit-label column-heading">{label}</span><span className="cove-icon">{tooltip}</span>
74
+ </label>
75
+ ))
76
+
77
+ const Select = memo(({label, value, options, fieldName, section = null, subsection = null, required = false, updateField, initial: initialValue, ...attributes}) => {
78
+ let optionsJsx = '';
79
+ if ( Array.isArray(options)) { //Handle basic array
80
+ optionsJsx = options.map(optionName => <option value={optionName} key={optionName}>{optionName}</option>)
81
+ } else { //Handle object with value/name pairs
82
+ optionsJsx = [];
83
+ for (const [optionValue, optionName] of Object.entries(options)) {
84
+ optionsJsx.push(<option value={optionValue} key={optionValue}>{optionName}</option>)
85
+ }
86
+ }
87
+
88
+ if(initialValue) {
89
+ optionsJsx.unshift(<option value="" key="initial">{initialValue}</option>)
90
+ }
91
+
92
+ return (
93
+ <label>
94
+ <span className="edit-label">{label}</span>
95
+ <select className={required && !value ? 'warning' : ''} name={fieldName} value={value} onChange={(event) => { updateField(section, subsection, fieldName, event.target.value) }} {...attributes}>
96
+ {optionsJsx}
97
+ </select>
98
+ </label>
99
+ )
100
+ })
101
+
102
+ const headerColors = ['theme-blue','theme-purple','theme-brown','theme-teal','theme-pink','theme-orange','theme-slate','theme-indigo','theme-cyan','theme-green','theme-amber']
103
+
104
+ const EditorPanel = memo(() => {
105
+ const {
106
+ config,
107
+ updateConfig,
108
+ loading,
109
+ data,
110
+ setParentConfig,
111
+ isDashboard,
112
+ } = useContext(Context);
113
+
114
+ const [ displayPanel, setDisplayPanel ] = useState(true);
115
+ const enforceRestrictions = (updatedConfig) => {
116
+ //If there are any dependencies between fields, etc../
117
+ };
118
+
119
+ const updateField = (section, subsection, fieldName, newValue) => {
120
+ // Top level
121
+ if( null === section && null === subsection) {
122
+ let updatedConfig = {...config, [fieldName]: newValue};
123
+
124
+ if ( 'filterColumn' === fieldName ) {
125
+ updatedConfig.filterValue = '';
126
+ }
127
+
128
+ enforceRestrictions(updatedConfig);
129
+
130
+ updateConfig(updatedConfig);
131
+ return
132
+ }
133
+
134
+ const isArray = Array.isArray(config[section]);
135
+
136
+ let sectionValue = isArray ? [...config[section], newValue] : {...config[section], [fieldName]: newValue};
137
+
138
+ if(null !== subsection) {
139
+ if(isArray) {
140
+ sectionValue = [...config[section]]
141
+ sectionValue[subsection] = {...sectionValue[subsection], [fieldName]: newValue}
142
+ } else if(typeof newValue === "string") {
143
+ sectionValue[subsection] = newValue
144
+ } else {
145
+ sectionValue = {...config[section], [subsection]: { ...config[section][subsection], [fieldName]: newValue}}
146
+ }
147
+ }
148
+
149
+ let updatedConfig = {...config, [section]: sectionValue};
150
+
151
+ enforceRestrictions(updatedConfig);
152
+
153
+ updateConfig(updatedConfig)
154
+ }
155
+
156
+ const missingRequiredSections = () => {
157
+ //Whether to show error message if something is required to show a data-bite and isn't filled in
158
+ return false;
159
+ };
160
+
161
+ useEffect(() => {
162
+ // Pass up to Editor if needed
163
+ if(setParentConfig) {
164
+ const newConfig = convertStateToConfig()
165
+
166
+ setParentConfig(newConfig)
167
+ }
168
+ // eslint-disable-next-line react-hooks/exhaustive-deps
169
+ }, [config])
170
+
171
+ const onBackClick = () => {
172
+ setDisplayPanel(!displayPanel);
173
+ }
174
+
175
+ const Error = () => {
176
+ return (
177
+ <section className="waiting">
178
+ <section className="waiting-container">
179
+ <h3>Error With Configuration</h3>
180
+ <p>{config.runtime.editorErrorMessage}</p>
181
+ </section>
182
+ </section>
183
+ );
184
+ }
185
+
186
+ const Confirm = () => {
187
+ return (
188
+ <section className="waiting">
189
+ <section className="waiting-container">
190
+ <h3>Finish Configuring</h3>
191
+ <p>Set all required options to the left and confirm below to display a preview of the chart.</p>
192
+ <button className="btn" style={{margin: '1em auto'}} disabled={missingRequiredSections()} onClick={(e) => {e.preventDefault(); updateConfig({...config, newViz: false})}}>I'm Done</button>
193
+ </section>
194
+ </section>
195
+ );
196
+ }
197
+
198
+ const convertStateToConfig = () => {
199
+ let strippedState = JSON.parse(JSON.stringify(config))
200
+ //if(false === missingRequiredSections()) {
201
+ //strippedState.newViz
202
+ //}
203
+ delete strippedState.runtime
204
+
205
+ return strippedState
206
+ }
207
+
208
+ // Filters -----------------------------------------------
209
+ const removeFilter = (index) => {
210
+ let filters = [...config.filters];
211
+
212
+ filters.splice(index, 1);
213
+
214
+ updateConfig({...config, filters})
215
+ }
216
+
217
+ const updateFilterProp = (name, index, value) => {
218
+ let filters = [...config.filters];
219
+
220
+ filters[index][name] = value;
221
+
222
+ updateConfig({...config, filters});
223
+ }
224
+
225
+ const addNewFilter = () => {
226
+ let filters = config.filters ? [...config.filters] : [];
227
+
228
+ filters.push({values: []});
229
+
230
+ updateConfig({...config, filters});
231
+ }
232
+
233
+ const getColumns = (filter = true) => {
234
+ let columns = {}
235
+ if(data.length){
236
+ data.map(row => {
237
+ return Object.keys(row).forEach(columnName => columns[columnName] = true)
238
+ })
239
+ }
240
+
241
+
242
+ return Object.keys(columns)
243
+ }
244
+
245
+ const getFilterColumnValues = (index) => {
246
+ let filterDataOptions = []
247
+ const filterColumnName = config.filters[index].columnName;
248
+ if (data && filterColumnName) {
249
+ data.forEach(function(row) {
250
+ if ( undefined !== row[filterColumnName] && -1 === filterDataOptions.indexOf(row[filterColumnName]) ) {
251
+ filterDataOptions.push(row[filterColumnName]);
252
+ }
253
+ })
254
+ filterDataOptions.sort();
255
+ }
256
+ return filterDataOptions;
257
+ }
258
+
259
+ // Dynamic Images ----------------------------------------
260
+ const updateDynamicImage = (name, index, subindex = null, value) => {
261
+ let imageOptions = [...config.imageData.options];
262
+ null === subindex ? imageOptions[index][name] = value : imageOptions[index].arguments[subindex][name] = value
263
+
264
+ let payload = {...config.imageData, options: imageOptions}
265
+ updateConfig({...config, imageData: payload});
266
+ }
267
+
268
+ const setDynamicArgument = (optionIndex, name, value) => {
269
+ let imageArguments = [...config.imageData.options[optionIndex].arguments]
270
+ imageArguments[1] = {...imageArguments[1], [name]: value }
271
+ let argumentsPayload = {...config.imageData.options[optionIndex], arguments: imageArguments}
272
+ let optionsPayload = [...config.imageData.options]
273
+ optionsPayload[optionIndex] = argumentsPayload
274
+ let payload = {...config.imageData, options: optionsPayload}
275
+ updateConfig({...config, imageData: payload})
276
+ }
277
+
278
+ const removeDynamicArgument = (optionIndex) => {
279
+ if (config.imageData.options[optionIndex].arguments.length > 1) {
280
+ let imageArguments = [...config.imageData.options[optionIndex].arguments]
281
+ imageArguments.pop()
282
+ let argumentsPayload = {...config.imageData.options[optionIndex], arguments: imageArguments}
283
+ let optionsPayload = [...config.imageData.options]
284
+ optionsPayload[optionIndex] = argumentsPayload
285
+ let payload = {...config.imageData, options: optionsPayload}
286
+ updateConfig({...config, imageData: payload})
287
+ }
288
+ }
289
+
290
+ const addDynamicImage = () => {
291
+ let imageOptions = config.imageData.options ? [ ...config.imageData.options ] : []
292
+ imageOptions.push({ source: '', arguments: [{ operator: '', threshold: ''}], alt: '', secondArgument: false })
293
+
294
+ let payload = {...config.imageData, options: imageOptions}
295
+ updateConfig({...config, imageData: payload})
296
+ }
297
+
298
+ const removeDynamicImage = (index) => {
299
+ let imageOptions = [...config.imageData.options];
300
+ imageOptions.splice(index, 1);
301
+
302
+ let payload = {...config.imageData, options: imageOptions}
303
+ updateConfig({...config, imageData: payload});
304
+ }
305
+
306
+ // General -----------------------------------------------
307
+ if(loading) {
308
+ return null
309
+ }
310
+
311
+ return (
312
+ <ErrorBoundary component="EditorPanel">
313
+ {!config.newViz && config.runtime && config.runtime.editorErrorMessage && <Error /> }
314
+ {(!config.dataColumn || !config.dataFunction) && <Confirm />}
315
+ <button className={displayPanel ? `editor-toggle` : `editor-toggle collapsed`} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={onBackClick} />
316
+ <section className={displayPanel ? 'editor-panel cove' : 'hidden editor-panel cove'}>
317
+ <div className="heading-2">Configure Data Bite</div>
318
+ <section className="form-container">
319
+ <form>
320
+ <Accordion allowZeroExpanded={true}>
321
+ <AccordionItem> {/* General */}
322
+ <AccordionItemHeading>
323
+ <AccordionItemButton>
324
+ General
325
+ </AccordionItemButton>
326
+ </AccordionItemHeading>
327
+ <AccordionItemPanel>
328
+ <Select value={config.biteStyle} fieldName="biteStyle" label="Data Bite Style" updateField={updateField} options={BITE_LOCATIONS} initial="Select" />
329
+ <TextField value={config.title} fieldName="title" label="Title" placeholder="Data Bite Title" updateField={updateField} />
330
+ <TextField type="textarea" value={config.biteBody} fieldName="biteBody" label="Message" updateField={updateField} tooltip={
331
+ <Tooltip style={{textTransform: 'none'}}>
332
+ <Tooltip.Target><Icon display="question" style={{marginLeft: '0.5rem'}}/></Tooltip.Target>
333
+ <Tooltip.Content>
334
+ <p>Enter the message text for the visualization. The following HTML tags are supported: strong, em, sup, and sub.</p>
335
+ </Tooltip.Content>
336
+ </Tooltip>
337
+ }/>
338
+ <TextField value={config.subtext} fieldName="subtext" label="Subtext/Citation" placeholder="Data Bite Subtext or Citation" updateField={updateField} tooltip={
339
+ <Tooltip style={{textTransform: 'none'}}>
340
+ <Tooltip.Target><Icon display="question" style={{marginLeft: '0.5rem'}}/></Tooltip.Target>
341
+ <Tooltip.Content>
342
+ <p>
343
+ Enter supporting text to display below the data visualization, if applicable. The following HTML tags are supported: strong, em, sup, and sub.</p>
344
+ </Tooltip.Content>
345
+ </Tooltip>
346
+ }/>
347
+ <CheckBox value={config.general.isCompactStyle} section="general" fieldName="isCompactStyle" label="Compact Style" updateField={updateField} tooltip={
348
+ <Tooltip style={{ textTransform: 'none' }}>
349
+ <Tooltip.Target><Icon display="question" style={{ marginLeft: '0.5rem' }} /></Tooltip.Target>
350
+ <Tooltip.Content>
351
+ <p>Simple data bite style that formats certain elements for a more compact view.</p>
352
+ </Tooltip.Content>
353
+ </Tooltip>
354
+ } />
355
+ </AccordionItemPanel>
356
+ </AccordionItem>
357
+
358
+ <AccordionItem> {/*Data*/}
359
+ <AccordionItemHeading>
360
+ <AccordionItemButton>
361
+ Data {(!config.dataColumn || !config.dataFunction) && <WarningImage width="25" className="warning-icon" />}
362
+ </AccordionItemButton>
363
+ </AccordionItemHeading>
364
+ <AccordionItemPanel>
365
+ <ul className="column-edit">
366
+ <li className="two-col">
367
+ <Select value={config.dataColumn || ""} fieldName="dataColumn" label="Data Column" updateField={updateField} initial="Select" required={true} options={getColumns()} />
368
+ <Select value={config.dataFunction || ""} fieldName="dataFunction" label="Data Function" updateField={updateField} initial="Select" required={true} options={DATA_FUNCTIONS} />
369
+ </li>
370
+ </ul>
371
+ <span className="divider-heading">Number Formatting</span>
372
+ <ul className="column-edit">
373
+ <li className="three-col">
374
+ <TextField value={config.dataFormat.prefix} section="dataFormat" fieldName="prefix" label="Prefix" updateField={updateField} />
375
+ <TextField value={config.dataFormat.suffix} section="dataFormat" fieldName="suffix" label="Suffix" updateField={updateField} />
376
+ <TextField type="number" value={config.dataFormat.roundToPlace} section="dataFormat" fieldName="roundToPlace" label="Round" updateField={updateField} min='0' max='99' />
377
+ </li>
378
+ </ul>
379
+ <CheckBox value={config.dataFormat.commas} section="dataFormat" fieldName="commas" label="Add commas" updateField={updateField} />
380
+ <CheckBox value={config.dataFormat.ignoreZeros} section="dataFormat" fieldName="ignoreZeros" label="Ignore Zeros" updateField={updateField} />
381
+ <hr className="accordion__divider" />
382
+
383
+ <label style={{marginBottom: '1rem'}}>
384
+ <span className="edit-label">
385
+ Data Point Filters
386
+ <Tooltip style={{ textTransform: 'none' }}>
387
+ <Tooltip.Target><Icon display="question" style={{ marginLeft: '0.5rem' }}/></Tooltip.Target>
388
+ <Tooltip.Content>
389
+ <p>To refine the highlighted data point, specify one or more filters (e.g., "Male" and
390
+ "Female" for a column called "Sex").</p>
391
+ </Tooltip.Content>
392
+ </Tooltip>
393
+ </span>
394
+ </label>
395
+ {
396
+ config.filters &&
397
+ <ul className="filters-list">
398
+ {config.filters.map((filter, index) => (
399
+ <fieldset className="edit-block" key={index}>
400
+ <button type="button" className="remove-column" onClick={() => {removeFilter(index)}}>Remove</button>
401
+ <label>
402
+ <span className="edit-label column-heading">Column</span>
403
+ <select value={filter.columnName ? filter.columnName : ''} onChange={(e) => {updateFilterProp('columnName', index, e.target.value)}}>
404
+ <option value="">- Select Option -</option>
405
+ {getColumns().map((dataKey, index) => (
406
+ <option value={dataKey} key={index}>{dataKey}</option>
407
+ ))}
408
+ </select>
409
+ </label>
410
+ <label>
411
+ <span className="edit-label column-heading">Column Value</span>
412
+ <select value={filter.columnValue} onChange={(e) => {updateFilterProp('columnValue', index, e.target.value)}}>
413
+ <option value="">- Select Option -</option>
414
+ {getFilterColumnValues(index).map((dataKey, index) => (
415
+ <option value={dataKey} key={index}>{dataKey}</option>
416
+ ))}
417
+ </select>
418
+ </label>
419
+ </fieldset>
420
+ ))}
421
+ </ul>
422
+ }
423
+ {(!config.filters || config.filters.length === 0) &&
424
+ <div>
425
+ <fieldset className="edit-block">
426
+ <p style={{textAlign: "center"}}>There are currently no filters.</p>
427
+ </fieldset>
428
+ </div>
429
+ }
430
+ <button type="button" onClick={addNewFilter} className="btn full-width">Add Filter</button>
431
+ </AccordionItemPanel>
432
+ </AccordionItem>
433
+
434
+ <AccordionItem> {/*Visual*/}
435
+ <AccordionItemHeading>
436
+ <AccordionItemButton>
437
+ Visual
438
+ </AccordionItemButton>
439
+ </AccordionItemHeading>
440
+ <AccordionItemPanel>
441
+ <TextField type="number" value={config.biteFontSize} fieldName="biteFontSize" label="Bite Font Size" updateField={updateField} min="16" max="65" />
442
+ <Select value={config.fontSize} fieldName="fontSize" label="Overall Font Size" updateField={updateField} options={['small', 'medium', 'large']} />
443
+ <CheckBox value={config.shadow} fieldName="shadow" label="Display Shadow" updateField={updateField} />
444
+ <label className="header">
445
+ <span className="edit-label">Theme</span>
446
+ <ul className="color-palette">
447
+ {headerColors.map( (palette) => (
448
+ <li title={ palette } key={ palette } onClick={ () => { updateConfig({...config, theme: palette})}} className={ config.theme === palette ? "selected " + palette : palette} />
449
+ ))}
450
+ </ul>
451
+ </label>
452
+ {/* <div className="cove-accordion__panel-section">
453
+ <CheckBox value={config.visual.border} section="visual" fieldName="border" label="Display Border" updateField={updateField} />
454
+ <CheckBox value={config.visual.borderColorTheme} section="visual" fieldName="borderColorTheme" label="Use Border Color Theme" updateField={updateField} />
455
+ <CheckBox value={config.visual.accent} section="visual" fieldName="accent" label="Use Accent Style" updateField={updateField} />
456
+ <CheckBox value={config.visual.background} section="visual" fieldName="background" label="Use Theme Background Color" updateField={updateField} />
457
+ <CheckBox value={config.visual.hideBackgroundColor} section="visual" fieldName="hideBackgroundColor" label="Hide Background Color" updateField={updateField} />
458
+ </div> */}
459
+ </AccordionItemPanel>
460
+ </AccordionItem>
461
+
462
+ {['title', 'body', 'graphic'].includes(config.biteStyle) &&
463
+ <AccordionItem> {/*Image & Dynamic Images*/}
464
+ <AccordionItemHeading>
465
+ <AccordionItemButton>
466
+ Image{[ 'dynamic' ].includes(config.imageData.display) && 's'}
467
+ </AccordionItemButton>
468
+ </AccordionItemHeading>
469
+
470
+ <AccordionItemPanel>
471
+ <Select value={config.imageData.display || ""} section="imageData" fieldName="display" label="Image Display Type" updateField={updateField} options={['none', 'static', 'dynamic']} />
472
+ <Select value={config.bitePosition || ""} fieldName="bitePosition" label="Image/Graphic Position" updateField={updateField} initial="Select" options={IMAGE_POSITIONS} />
473
+ {['static'].includes(config.imageData.display) &&
474
+ <>
475
+ <TextField value={config.imageData.url} section="imageData" fieldName="url" label="Image URL" updateField={updateField} />
476
+ <TextField value={config.imageData.alt} section="imageData" fieldName="alt" label="Alt Text" updateField={updateField} />
477
+ </>
478
+ }
479
+
480
+ {[ 'dynamic' ].includes(config.imageData.display) &&
481
+ <>
482
+ <TextField value={config.imageData.url || ""} section="imageData" fieldName="url" label="Image URL (default)" updateField={updateField} />
483
+ <TextField value={config.imageData.alt} section="imageData" fieldName="alt" label="Alt Text (default)" updateField={updateField} />
484
+
485
+ <hr className="accordion__divider" />
486
+
487
+ {(!config.imageData.options || config.imageData.options.length === 0) && <p style={{textAlign: "center"}}>There are currently no dynamic images.</p>}
488
+ {config.imageData.options && config.imageData.options.length > 0 &&
489
+ <>
490
+ <ul>
491
+ {config.imageData.options.map((option, index) => (
492
+ <fieldset className="edit-block" key={index}>
493
+ <button type="button" className="remove-column" onClick={() => {removeDynamicImage(index)}}>Remove</button>
494
+ <label>
495
+ <span className="edit-label column-heading"><strong>{'Image #' + (index + 1)}</strong></span>
496
+
497
+ <div className="accordion__panel-row align-center">
498
+ <div className="accordion__panel-col flex-auto">
499
+ If Value
500
+ </div>
501
+ <div className="accordion__panel-col flex-auto">
502
+ <select value={option.arguments[0]?.operator || ""} onChange={(e) => {updateDynamicImage('operator', index, 0, e.target.value)}}>
503
+ <option value="" disabled/>
504
+ {DATA_OPERATORS.map((operator, index) => (
505
+ <option value={operator} key={index}>{operator}</option>
506
+ ))}
507
+ </select>
508
+ </div>
509
+ <div className="accordion__panel-col flex-grow flex-shrink">
510
+ <input type="number" value={option.arguments[0]?.threshold || ""} onChange={(e) => {updateDynamicImage('threshold', index, 0, e.target.value)}} />
511
+ </div>
512
+ </div>
513
+
514
+ <div className="accordion__panel-row mb-2 align-center">
515
+ <div className="accordion__panel-col flex-grow">
516
+ <select className='border-dashed text-center' value={option.secondArgument ? 'and' : 'then'} onChange={(e) => {
517
+ if ('then' === e.target.value) {updateDynamicImage('secondArgument', index, null,false); removeDynamicArgument(index)}
518
+ if ('and' === e.target.value) {updateDynamicImage('secondArgument', index, null,true)}
519
+ }}>
520
+ <option value={'then'}>Then</option>
521
+ <option value={'and'}>And</option>
522
+ </select>
523
+ </div>
524
+ </div>
525
+
526
+ {option.secondArgument && true === option.secondArgument &&
527
+ <>
528
+ <div className="accordion__panel-row align-center">
529
+ <div className="accordion__panel-col flex-auto">
530
+ If Value
531
+ </div>
532
+ <div className="accordion__panel-col flex-auto">
533
+ <select value={option.arguments[1]?.operator || ""} onChange={(e) => {setDynamicArgument(index, 'operator', e.target.value)}}>
534
+ <option value="" disabled/>
535
+ {DATA_OPERATORS.map((operator, index) => (
536
+ <option value={operator} key={index}>{operator}</option>
537
+ ))}
538
+ </select>
539
+ </div>
540
+ <div className="accordion__panel-col flex-grow flex-shrink">
541
+ <input type="number" value={option.arguments[1]?.threshold || ""} onChange={(e) => {setDynamicArgument(index, 'threshold', e.target.value)}} />
542
+ </div>
543
+ </div>
544
+ <div className="accordion__panel-row mb-2 align-center text-center text-capitalize">
545
+ <div className="accordion__panel-col flex-grow">
546
+ Then
547
+ </div>
548
+ </div>
549
+ </>
550
+ }
551
+
552
+ <div className="accordion__panel-row mb-2 align-center">
553
+ <div className="accordion__panel-col flex-auto">
554
+ Show
555
+ </div>
556
+ <div className="accordion__panel-col flex-grow">
557
+ <input type="text" value={option.source || ""} onChange={(e) => {updateDynamicImage('source', index, null, e.target.value)}} />
558
+ </div>
559
+ </div>
560
+
561
+ <div className="accordion__panel-row mb-2 align-center">
562
+ <div className="accordion__panel-col flex-auto">
563
+ Alt Text
564
+ </div>
565
+ <div className="accordion__panel-col flex-grow">
566
+ <input type="text" value={option.alt || ""} onChange={(e) => {updateDynamicImage('alt', index, null, e.target.value)}} />
567
+ </div>
568
+ </div>
569
+ </label>
570
+ </fieldset>
571
+ ))}
572
+ </ul>
573
+ </>
574
+ }
575
+ <button type="button" onClick={addDynamicImage} className="btn full-width">Add Dynamic Image</button>
576
+ </>
577
+ }
578
+ </AccordionItemPanel>
579
+ </AccordionItem>
580
+ }
581
+
582
+ </Accordion>
583
+ </form>
584
+ </section>
585
+ </section>
586
+ </ErrorBoundary>
587
+ )
588
+ })
589
+
590
+ export default EditorPanel;
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react';
2
+
3
+ const ConfigContext = createContext({});
4
+
5
+ export default ConfigContext;
@@ -0,0 +1,39 @@
1
+ export default {
2
+ type: "data-bite",
3
+ data: [],
4
+ dataBite: "",
5
+ dataFunction: "",
6
+ dataColumn: "",
7
+ bitePosition: "Left",
8
+ biteFontSize: 24,
9
+ fontSize: "medium",
10
+ biteBody: "",
11
+ imageData: {
12
+ display: "none",
13
+ url: "",
14
+ alt: "",
15
+ options: []
16
+ },
17
+ dataFormat: {
18
+ roundToPlace: 0,
19
+ commas: true,
20
+ prefix: "",
21
+ suffix: "%"
22
+ },
23
+ biteStyle: "graphic",
24
+ filters: [],
25
+ subtext: "",
26
+ title: "",
27
+ theme: "theme-blue",
28
+ shadow: false,
29
+ visual: {
30
+ border: false,
31
+ accent: false,
32
+ background: false,
33
+ hideBackgroundColor: false,
34
+ borderColorTheme: false
35
+ },
36
+ general: {
37
+ isCompactStyle: false
38
+ }
39
+ }
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-2 -2)" fill="none" fill-rule="evenodd"><path d="M0 0h24v24H0z"/><circle fill="#FFF" cx="12" cy="12" r="8"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#4C4C4C" fill-rule="nonzero"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 10 5" xmlns="http://www.w3.org/2000/svg"><path d="M0 5l5-5 5 5z" fill="#FFF" fill-rule="nonzero"/></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 10 5" xmlns="http://www.w3.org/2000/svg"><path d="M0 0l5 5 5-5z" fill="#FFF" fill-rule="nonzero"/></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M-2-2h24v24H-2z"/><path d="M10 1.5A8.504 8.504 0 001.5 10c0 4.692 3.808 8.5 8.5 8.5s8.5-3.808 8.5-8.5-3.808-8.5-8.5-8.5z" stroke="#4C4C4C" stroke-width="3"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/></svg>