@cdc/dashboard 1.1.1 → 1.1.4
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/dist/cdcdashboard.js +378 -0
- package/examples/default-data.json +368 -0
- package/examples/default.json +156 -0
- package/examples/test-example.json +1 -0
- package/package.json +13 -11
- package/src/CdcDashboard.js +491 -0
- package/src/components/Column.js +46 -0
- package/src/components/DataTable.tsx +172 -0
- package/src/components/EditorPanel.js +354 -0
- package/src/components/Grid.js +28 -0
- package/src/components/Header.js +15 -0
- package/src/components/Row.js +137 -0
- package/src/components/Widget.js +112 -0
- package/src/context.tsx +5 -0
- package/src/data/initial-state.js +17 -0
- package/src/images/icon-close.svg +1 -0
- package/src/images/icon-code.svg +3 -0
- package/src/images/icon-col-12.svg +3 -0
- package/src/images/icon-col-4-8.svg +3 -0
- package/src/images/icon-col-4.svg +3 -0
- package/src/images/icon-col-6.svg +3 -0
- package/src/images/icon-col-8-4.svg +3 -0
- package/src/images/icon-down.svg +1 -0
- package/src/images/icon-edit.svg +3 -0
- package/src/images/icon-grid.svg +4 -0
- package/src/images/icon-move.svg +8 -0
- package/src/images/icon-up.svg +1 -0
- package/src/images/warning.svg +1 -0
- package/src/index.html +27 -0
- package/src/index.js +17 -0
- package/src/scss/editor-panel.scss +652 -0
- package/src/scss/grid.scss +321 -0
- package/src/scss/main.scss +214 -0
- package/src/scss/mixins.scss +0 -0
- package/src/scss/variables.scss +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import React, { useState, useEffect, memo, useContext } from 'react'
|
|
2
|
+
import ReactTooltip from 'react-tooltip'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Accordion,
|
|
6
|
+
AccordionItem,
|
|
7
|
+
AccordionItemHeading,
|
|
8
|
+
AccordionItemPanel,
|
|
9
|
+
AccordionItemButton,
|
|
10
|
+
} from 'react-accessible-accordion';
|
|
11
|
+
import { useDebounce } from 'use-debounce';
|
|
12
|
+
|
|
13
|
+
import Context from '../context';
|
|
14
|
+
|
|
15
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
|
|
16
|
+
import QuestionIcon from '@cdc/core/assets/question-circle.svg';
|
|
17
|
+
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
18
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
19
|
+
|
|
20
|
+
const Helper = ({text}) => {
|
|
21
|
+
return (
|
|
22
|
+
<span className='tooltip helper' data-tip={text}>
|
|
23
|
+
<QuestionIcon />
|
|
24
|
+
</span>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// IE11 Custom Event polyfill
|
|
29
|
+
(function () {
|
|
30
|
+
|
|
31
|
+
if ( typeof window.CustomEvent === "function" ) return false;
|
|
32
|
+
|
|
33
|
+
function CustomEvent ( event, params ) {
|
|
34
|
+
params = params || { bubbles: false, cancelable: false, detail: null };
|
|
35
|
+
var evt = document.createEvent( 'CustomEvent' );
|
|
36
|
+
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
|
|
37
|
+
return evt;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
window.CustomEvent = CustomEvent;
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
const TextField = memo(({label, section = null, subsection = null, fieldName, updateField, value: stateValue, tooltip, type = "input", i = null, min = null, ...attributes}) => {
|
|
44
|
+
const [ value, setValue ] = useState(stateValue);
|
|
45
|
+
|
|
46
|
+
const [ debouncedValue ] = useDebounce(value, 500);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if('string' === typeof debouncedValue && stateValue !== debouncedValue ) {
|
|
50
|
+
updateField(section, subsection, fieldName, debouncedValue, i)
|
|
51
|
+
}
|
|
52
|
+
}, [debouncedValue])
|
|
53
|
+
|
|
54
|
+
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`;
|
|
55
|
+
|
|
56
|
+
const onChange = (e) => {
|
|
57
|
+
if('number' !== type || min === null){
|
|
58
|
+
setValue(e.target.value);
|
|
59
|
+
} else {
|
|
60
|
+
if(!e.target.value || min <= parseFloat(e.target.value)){
|
|
61
|
+
setValue(e.target.value);
|
|
62
|
+
} else {
|
|
63
|
+
setValue(min.toString());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let formElement = <input type="text" name={name} onChange={onChange} {...attributes} value={value} />
|
|
69
|
+
|
|
70
|
+
if('textarea' === type) {
|
|
71
|
+
formElement = (
|
|
72
|
+
<textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if('number' === type) {
|
|
77
|
+
formElement = <input type="number" name={name} onChange={onChange} {...attributes} value={value} />
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<label>
|
|
82
|
+
<span className="edit-label column-heading">{label}{tooltip}</span>
|
|
83
|
+
{formElement}
|
|
84
|
+
</label>
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const CheckBox = memo(({label, value, fieldName, section = null, subsection = null, updateField, ...attributes}) => (
|
|
89
|
+
<label className="checkbox">
|
|
90
|
+
<input type="checkbox" name={fieldName} checked={ value } onChange={() => { updateField(section, subsection, fieldName, !value) }} {...attributes}/>
|
|
91
|
+
<span className="edit-label">{label}</span>
|
|
92
|
+
{section === 'table' && fieldName === 'show' && <Helper text=" Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a 508 requirement." />}
|
|
93
|
+
</label>
|
|
94
|
+
))
|
|
95
|
+
|
|
96
|
+
const Select = memo(({label, value, options, fieldName, section = null, subsection = null, required = false, updateField, initial: initialValue, ...attributes}) => {
|
|
97
|
+
let optionsJsx = options.map(optionName => <option value={optionName} key={optionName}>{optionName}</option>)
|
|
98
|
+
|
|
99
|
+
if(initialValue) {
|
|
100
|
+
optionsJsx.unshift(<option value="" key="initial">{initialValue}</option>)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<label>
|
|
105
|
+
<span className="edit-label">{label}</span>
|
|
106
|
+
<select className={required && !value ? 'warning' : ''} name={fieldName} value={value} onChange={(event) => { updateField(section, subsection, fieldName, event.target.value) }} {...attributes}>
|
|
107
|
+
{optionsJsx}
|
|
108
|
+
</select>
|
|
109
|
+
</label>
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const EditorPanel = memo(() => {
|
|
114
|
+
const {
|
|
115
|
+
config,
|
|
116
|
+
updateConfig,
|
|
117
|
+
loading,
|
|
118
|
+
rawData,
|
|
119
|
+
setParentConfig,
|
|
120
|
+
setEditing
|
|
121
|
+
} = useContext(Context);
|
|
122
|
+
|
|
123
|
+
const enforceRestrictions = (updatedConfig) => {
|
|
124
|
+
// TODO
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const updateField = (section, subsection, fieldName, newValue) => {
|
|
128
|
+
// Top level
|
|
129
|
+
if( null === section && null === subsection) {
|
|
130
|
+
let dashboardConfig = config.dashboard;
|
|
131
|
+
|
|
132
|
+
dashboardConfig[fieldName] = newValue;
|
|
133
|
+
|
|
134
|
+
let updatedConfig = {...config, dashboard: dashboardConfig};
|
|
135
|
+
|
|
136
|
+
enforceRestrictions(updatedConfig);
|
|
137
|
+
|
|
138
|
+
updateConfig(updatedConfig);
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const isArray = Array.isArray(config[section]);
|
|
143
|
+
|
|
144
|
+
let sectionValue = isArray ? [...config[section], newValue] : {...config[section], [fieldName]: newValue};
|
|
145
|
+
|
|
146
|
+
if(null !== subsection) {
|
|
147
|
+
if(isArray) {
|
|
148
|
+
sectionValue = [...config[section]]
|
|
149
|
+
sectionValue[subsection] = {...sectionValue[subsection], [fieldName]: newValue}
|
|
150
|
+
} else if(typeof newValue === "string") {
|
|
151
|
+
sectionValue[subsection] = newValue
|
|
152
|
+
} else {
|
|
153
|
+
sectionValue = {...config[section], [subsection]: { ...config[section][subsection], [fieldName]: newValue}}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let updatedConfig = {...config, [section]: sectionValue};
|
|
158
|
+
|
|
159
|
+
enforceRestrictions(updatedConfig);
|
|
160
|
+
|
|
161
|
+
updateConfig(updatedConfig)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const missingRequiredSections = () => {
|
|
165
|
+
//TODO
|
|
166
|
+
|
|
167
|
+
return false;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const [ displayPanel, setDisplayPanel ] = useState(true);
|
|
171
|
+
|
|
172
|
+
// Used to pipe a JSON version of the config you are creating out
|
|
173
|
+
const [ configData, setConfigData ] = useState({})
|
|
174
|
+
|
|
175
|
+
if(loading) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const getColumns = (filter = true) => {
|
|
180
|
+
let columns = {}
|
|
181
|
+
|
|
182
|
+
rawData.map(row => {
|
|
183
|
+
Object.keys(row).forEach(columnName => columns[columnName] = true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
if(filter) {
|
|
187
|
+
Object.keys(columns).forEach(key => {
|
|
188
|
+
if((config.series && config.series.filter(series => series.dataKey === key).length > 0) || (config.confidenceKeys && Object.keys(config.confidenceKeys).includes(key)) ) {
|
|
189
|
+
delete columns[key]
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Object.keys(columns)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const Error = () => {
|
|
198
|
+
return (
|
|
199
|
+
<section className="waiting">
|
|
200
|
+
<section className="waiting-container">
|
|
201
|
+
<h3>Error With Configuration</h3>
|
|
202
|
+
<p>{config.runtime.editorErrorMessage}</p>
|
|
203
|
+
</section>
|
|
204
|
+
</section>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const convertStateToConfig = (type = "JSON") => {
|
|
209
|
+
let strippedState = JSON.parse(JSON.stringify(config))
|
|
210
|
+
if(false === missingRequiredSections()) {
|
|
211
|
+
delete strippedState.newViz
|
|
212
|
+
}
|
|
213
|
+
delete strippedState.runtime
|
|
214
|
+
|
|
215
|
+
if(type === "JSON") {
|
|
216
|
+
return JSON.stringify( strippedState )
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return strippedState
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
const parsedData = convertStateToConfig()
|
|
224
|
+
|
|
225
|
+
const formattedData = JSON.stringify(JSON.parse(parsedData), undefined, 2);
|
|
226
|
+
|
|
227
|
+
setConfigData(formattedData)
|
|
228
|
+
|
|
229
|
+
// Emit the data in a regular JS event so it can be consumed by anything.
|
|
230
|
+
const event = new CustomEvent('updateVizConfig', { detail: parsedData})
|
|
231
|
+
|
|
232
|
+
window.dispatchEvent(event)
|
|
233
|
+
|
|
234
|
+
// Pass up to Editor if needed
|
|
235
|
+
if(setParentConfig) {
|
|
236
|
+
const newConfig = convertStateToConfig("object")
|
|
237
|
+
setParentConfig(newConfig)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
241
|
+
}, [config]);
|
|
242
|
+
|
|
243
|
+
const removeFilter = (index) => {
|
|
244
|
+
let dashboardConfig = config.dashboard;
|
|
245
|
+
|
|
246
|
+
dashboardConfig.filters.splice(index, 1);
|
|
247
|
+
|
|
248
|
+
updateConfig({...config, dashboard: dashboardConfig});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const updateFilterProp = (name, index, value) => {
|
|
252
|
+
let dashboardConfig = config.dashboard;
|
|
253
|
+
|
|
254
|
+
dashboardConfig.filters[index][name] = value;
|
|
255
|
+
|
|
256
|
+
updateConfig({...config, dashboard: dashboardConfig});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const addNewFilter = () => {
|
|
260
|
+
let dashboardConfig = config.dashboard;
|
|
261
|
+
|
|
262
|
+
dashboardConfig.filters = dashboardConfig.filters || [];
|
|
263
|
+
|
|
264
|
+
dashboardConfig.filters.push({values: []});
|
|
265
|
+
|
|
266
|
+
updateConfig({...config, dashboard: dashboardConfig});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<ErrorBoundary component="EditorPanel">
|
|
271
|
+
{config.runtime && config.runtime.editorErrorMessage && <Error /> }
|
|
272
|
+
<button className={displayPanel ? `editor-toggle` : `editor-toggle collapsed`} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={() => setDisplayPanel(!displayPanel) }></button>
|
|
273
|
+
<section className={displayPanel ? 'editor-panel cove' : 'hidden editor-panel cove'}>
|
|
274
|
+
<div className="heading-2">Configure</div>
|
|
275
|
+
<section className="form-container">
|
|
276
|
+
<form>
|
|
277
|
+
<Accordion allowZeroExpanded={true}>
|
|
278
|
+
<AccordionItem> {/* General */}
|
|
279
|
+
<AccordionItemHeading>
|
|
280
|
+
<AccordionItemButton>
|
|
281
|
+
General
|
|
282
|
+
</AccordionItemButton>
|
|
283
|
+
</AccordionItemHeading>
|
|
284
|
+
<AccordionItemPanel>
|
|
285
|
+
<TextField value={config.dashboard.title} section="dashboard" fieldName="title" label="Title" updateField={updateField} />
|
|
286
|
+
<TextField type="textarea" value={config.dashboard.description} section="dashboard" fieldName="description" label="Description" updateField={updateField} tooltip={
|
|
287
|
+
<Tooltip style={{textTransform: 'none'}}>
|
|
288
|
+
<Tooltip.Target><Icon display="question" style={{marginLeft: '0.5rem'}}/></Tooltip.Target>
|
|
289
|
+
<Tooltip.Content>
|
|
290
|
+
<p>Enter supporting text to display below the data visualization, if applicable. The following HTML tags are supported: strong, em, sup, and sub.</p>
|
|
291
|
+
</Tooltip.Content>
|
|
292
|
+
</Tooltip>
|
|
293
|
+
}/>
|
|
294
|
+
</AccordionItemPanel>
|
|
295
|
+
</AccordionItem>
|
|
296
|
+
<AccordionItem>
|
|
297
|
+
<AccordionItemHeading>
|
|
298
|
+
<AccordionItemButton>
|
|
299
|
+
Filters
|
|
300
|
+
</AccordionItemButton>
|
|
301
|
+
</AccordionItemHeading>
|
|
302
|
+
<AccordionItemPanel>
|
|
303
|
+
<ul className="filters-list">
|
|
304
|
+
{config.dashboard.filters && config.dashboard.filters.map((filter, index) => (
|
|
305
|
+
<fieldset className="edit-block" key={filter.columnName + index}>
|
|
306
|
+
<button type="button" className="remove-column" onClick={() => {removeFilter(index)}}>Remove</button>
|
|
307
|
+
<label>
|
|
308
|
+
<span className="edit-label column-heading">Filter</span>
|
|
309
|
+
<select value={filter.columnName} onChange={(e) => {updateFilterProp('columnName', index, e.target.value)}}>
|
|
310
|
+
<option value="">- Select Option -</option>
|
|
311
|
+
{getColumns().map((dataKey) => (
|
|
312
|
+
<option value={dataKey} key={dataKey}>{dataKey}</option>
|
|
313
|
+
))}
|
|
314
|
+
</select>
|
|
315
|
+
</label>
|
|
316
|
+
<label>
|
|
317
|
+
<span className="edit-label column-heading">Label</span>
|
|
318
|
+
<input type="text" value={filter.label} onChange={(e) => {updateFilterProp('label', index, e.target.value)}}/>
|
|
319
|
+
</label>
|
|
320
|
+
</fieldset>
|
|
321
|
+
)
|
|
322
|
+
)}
|
|
323
|
+
</ul>
|
|
324
|
+
|
|
325
|
+
<button type="button" onClick={addNewFilter} className="btn btn-primary">Add Filter</button>
|
|
326
|
+
</AccordionItemPanel>
|
|
327
|
+
</AccordionItem>
|
|
328
|
+
<AccordionItem>
|
|
329
|
+
<AccordionItemHeading>
|
|
330
|
+
<AccordionItemButton>
|
|
331
|
+
Data Table
|
|
332
|
+
</AccordionItemButton>
|
|
333
|
+
</AccordionItemHeading>
|
|
334
|
+
<AccordionItemPanel>
|
|
335
|
+
<CheckBox value={config.table.show} section="table" fieldName="show" label="Show Table" updateField={updateField} />
|
|
336
|
+
<CheckBox value={config.table.expanded} section="table" fieldName="expanded" label="Expanded by Default" updateField={updateField} />
|
|
337
|
+
<CheckBox value={config.table.download} section="table" fieldName="download" label="Display Download Button" updateField={updateField} />
|
|
338
|
+
<TextField value={config.table.label} section="table" fieldName="label" label="Label" updateField={updateField} />
|
|
339
|
+
</AccordionItemPanel>
|
|
340
|
+
</AccordionItem>
|
|
341
|
+
</Accordion>
|
|
342
|
+
</form>
|
|
343
|
+
</section>
|
|
344
|
+
<ReactTooltip
|
|
345
|
+
html={true}
|
|
346
|
+
multiline={true}
|
|
347
|
+
className="helper-tooltip"
|
|
348
|
+
/>
|
|
349
|
+
</section>
|
|
350
|
+
</ErrorBoundary>
|
|
351
|
+
)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
export default EditorPanel;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import Row from './Row'
|
|
3
|
+
|
|
4
|
+
import Context from '../context'
|
|
5
|
+
|
|
6
|
+
const Grid = () => {
|
|
7
|
+
const { rows, config, updateConfig } = useContext(Context)
|
|
8
|
+
|
|
9
|
+
const addRow = () => {
|
|
10
|
+
updateConfig({
|
|
11
|
+
...config,
|
|
12
|
+
rows: [
|
|
13
|
+
...rows,
|
|
14
|
+
[{width: 12}, {}, {}]
|
|
15
|
+
],
|
|
16
|
+
uuid: Date.now()
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="builder-grid">
|
|
22
|
+
{rows.map((row, idx) => <Row row={row} idx={idx} uuid={row.uuid} key={idx}/>)}
|
|
23
|
+
<button className="btn add-row" onClick={addRow}>Add Row</button>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default Grid
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { useContext, memo } from 'react'
|
|
2
|
+
|
|
3
|
+
const Header = ({preview, setPreview, back, subEditor = null}) => {
|
|
4
|
+
return (
|
|
5
|
+
<div aria-level="2" role="heading" className="editor-heading">
|
|
6
|
+
{subEditor ? <div className="heading-1 back-to" onClick={back} style={{cursor: 'pointer'}}><span>←</span> Back to Dashboard</div> : <div className="heading-1">Dashboard Editor</div>}
|
|
7
|
+
{!subEditor && <ul className="toggle-bar">
|
|
8
|
+
<li className={preview ? 'inactive' : 'active'} onClick={() => {setPreview(false)}}>Build Layout</li>
|
|
9
|
+
<li className={preview ? 'active' : 'inactive'} onClick={() => {setPreview(true)}}>Preview & Configure</li>
|
|
10
|
+
</ul>}
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default Header
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { useContext, useState } from 'react'
|
|
2
|
+
import Column from './Column'
|
|
3
|
+
import Context from '../context'
|
|
4
|
+
import CloseIcon from '../images/icon-close.svg'
|
|
5
|
+
import RowUp from '../images/icon-up.svg'
|
|
6
|
+
import RowDown from '../images/icon-down.svg'
|
|
7
|
+
import OneColIcon from '../images/icon-col-12.svg'
|
|
8
|
+
import TwoColIcon from '../images/icon-col-6.svg'
|
|
9
|
+
import ThreeColIcon from '../images/icon-col-4.svg'
|
|
10
|
+
import FourEightColIcon from '../images/icon-col-4-8.svg'
|
|
11
|
+
import EightFourColIcon from '../images/icon-col-8-4.svg'
|
|
12
|
+
|
|
13
|
+
const RowMenu = ({ rowIdx, row }) => {
|
|
14
|
+
const { rows, config, updateConfig } = useContext(Context)
|
|
15
|
+
|
|
16
|
+
const getCurr = () => {
|
|
17
|
+
let res = []
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < row.length; i++) {
|
|
20
|
+
if(row[i].width) res.push(row[i].width)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return res.join('')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [curr, setCurr] = useState(getCurr())
|
|
27
|
+
|
|
28
|
+
const setRowLayout = (layout) => {
|
|
29
|
+
const newRows = [...rows]
|
|
30
|
+
const r = newRows[rowIdx]
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < r.length; i++) {
|
|
33
|
+
r[i].width = layout[i] ?? null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
updateConfig({ ...config, rows: newRows})
|
|
37
|
+
setCurr(layout.join(''))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const moveRow = (dir = 'down') => {
|
|
41
|
+
if (rowIdx === rows.length - 1 && dir === 'down') return
|
|
42
|
+
|
|
43
|
+
let newIdx = dir === 'down' ? rowIdx + 1 : rowIdx - 1
|
|
44
|
+
|
|
45
|
+
// Swap
|
|
46
|
+
const temp = rows[newIdx]
|
|
47
|
+
|
|
48
|
+
rows[newIdx] = row
|
|
49
|
+
rows[rowIdx] = temp
|
|
50
|
+
|
|
51
|
+
rows[newIdx].uuid = Date.now();
|
|
52
|
+
rows[rowIdx].uuid = Date.now();
|
|
53
|
+
|
|
54
|
+
updateConfig({...config, rows})
|
|
55
|
+
|
|
56
|
+
// TODO: Migrate this animation to a React animation library once one is selected for COVE. This is pretty minor so can stay for now.
|
|
57
|
+
let calcRowMove = dir === 'down' ? 202 : -202
|
|
58
|
+
let calcRowMove2 = dir === 'down' ? -202 : 202
|
|
59
|
+
|
|
60
|
+
let rowEle = document.querySelector('[data-row-id=\'' + rowIdx + '\']')
|
|
61
|
+
let rowNewEle = document.querySelector('[data-row-id=\'' + newIdx + '\']')
|
|
62
|
+
|
|
63
|
+
rowEle.style.pointerEvents = 'none'
|
|
64
|
+
rowNewEle.style.pointerEvents = 'none'
|
|
65
|
+
rowEle.style.top = calcRowMove + 'px'
|
|
66
|
+
rowNewEle.style.top = calcRowMove2 + 'px'
|
|
67
|
+
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
rowEle.style.transition = 'top 500ms cubic-bezier(0.16, 1, 0.3, 1)'
|
|
70
|
+
rowNewEle.style.transition = 'top 500ms cubic-bezier(0.16, 1, 0.3, 1)'
|
|
71
|
+
rowEle.style.top = '0'
|
|
72
|
+
rowNewEle.style.top = '0'
|
|
73
|
+
}, 0)
|
|
74
|
+
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
rowEle.style = null
|
|
77
|
+
rowNewEle.style = null
|
|
78
|
+
}, 500)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const deleteRow = () => {
|
|
82
|
+
rows.splice(rowIdx, 1) // Just delete the row. Don't delete the instantiated widgets for now.
|
|
83
|
+
|
|
84
|
+
updateConfig({...config, rows})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const layoutList = [
|
|
88
|
+
<li className={curr === '12' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 12 ])} key="12" title="1 Column">
|
|
89
|
+
<OneColIcon />
|
|
90
|
+
</li>,
|
|
91
|
+
<li className={curr === '66' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 6, 6 ])} key="66" title="2 Columns">
|
|
92
|
+
<TwoColIcon />
|
|
93
|
+
</li>,
|
|
94
|
+
<li className={curr === '444' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 4, 4, 4 ])} key="444" title="3 Columns">
|
|
95
|
+
<ThreeColIcon />
|
|
96
|
+
</li>,
|
|
97
|
+
<li className={curr === '48' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 4, 8 ])} key="48" title="2 Columns">
|
|
98
|
+
<FourEightColIcon />
|
|
99
|
+
</li>,
|
|
100
|
+
<li className={curr === '84' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([ 8, 4 ])} key="84" title="2 Columns">
|
|
101
|
+
<EightFourColIcon />
|
|
102
|
+
</li>
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<nav className="row-menu">
|
|
107
|
+
<div className="row-menu__btn">
|
|
108
|
+
<ul className="row-menu__flyout">
|
|
109
|
+
{layoutList}
|
|
110
|
+
</ul>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="spacer"></div>
|
|
113
|
+
<button className={rowIdx === 0 ? 'row-menu__btn row-menu__btn-disabled' : 'row-menu__btn'} title="Move Row Up" onClick={() => moveRow('up')}>
|
|
114
|
+
<RowUp />
|
|
115
|
+
</button>
|
|
116
|
+
<button className={rowIdx + 1 === rows.length ? 'row-menu__btn row-menu__btn-disabled' : 'row-menu__btn'} title="Move Row Down" onClick={() => moveRow('down')}>
|
|
117
|
+
<RowDown />
|
|
118
|
+
</button>
|
|
119
|
+
<button className={rowIdx === 0 && rows.length === 1 ? 'row-menu__btn row-menu__btn--remove row-menu__btn-disabled' : 'row-menu__btn row-menu__btn--remove'} title="Delete Row" onClick={deleteRow}>
|
|
120
|
+
<CloseIcon />
|
|
121
|
+
</button>
|
|
122
|
+
</nav>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const Row = ({ row, idx: rowIdx, uuid}) => {
|
|
127
|
+
return (
|
|
128
|
+
<div className="builder-row" data-row-id={rowIdx}>
|
|
129
|
+
<RowMenu rowIdx={rowIdx} row={row} />
|
|
130
|
+
<div className="column-container">
|
|
131
|
+
{row.filter(column => column.width).map((column, colIdx) => <Column data={column} key={`row-${uuid}-col-${colIdx}`} rowIdx={rowIdx} colIdx={colIdx} />)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default Row
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { useDrag } from 'react-dnd';
|
|
3
|
+
import CloseIcon from '../images/icon-close.svg';
|
|
4
|
+
import GridIcon from '../images/icon-grid.svg';
|
|
5
|
+
import CodeIcon from '../images/icon-code.svg';
|
|
6
|
+
import EditIcon from '../images/icon-edit.svg';
|
|
7
|
+
import MoveIcon from '../images/icon-move.svg';
|
|
8
|
+
import BiteIcon from '@cdc/core/assets/data-bite-graphic.svg';
|
|
9
|
+
import BarIcon from '@cdc/core/assets/chart-bar-solid.svg';
|
|
10
|
+
import LineIcon from '@cdc/core/assets/chart-line-solid.svg';
|
|
11
|
+
import PieIcon from '@cdc/core/assets/chart-pie-solid.svg';
|
|
12
|
+
import UsaIcon from '@cdc/core/assets/usa-graphic.svg';
|
|
13
|
+
import WorldIcon from '@cdc/core/assets/world-graphic.svg';
|
|
14
|
+
import AlabamaIcon from '@cdc/core/assets/alabama-graphic.svg';
|
|
15
|
+
|
|
16
|
+
import Context from '../context';
|
|
17
|
+
|
|
18
|
+
const iconHash = {
|
|
19
|
+
'data-bite' : <BiteIcon />,
|
|
20
|
+
'Bar': <BarIcon />,
|
|
21
|
+
'Spark Line': <LineIcon />,
|
|
22
|
+
'waffle-chart' : <GridIcon />,
|
|
23
|
+
'markup-include' : <CodeIcon />,
|
|
24
|
+
'Line' : <LineIcon />,
|
|
25
|
+
'Pie' : <PieIcon />,
|
|
26
|
+
'us' : <UsaIcon />,
|
|
27
|
+
'us-county': <UsaIcon />,
|
|
28
|
+
'world' : <WorldIcon />,
|
|
29
|
+
'single-state': <AlabamaIcon />
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const labelHash = {
|
|
33
|
+
'data-bite': 'Data Bite',
|
|
34
|
+
'waffle-chart' : 'Waffle Chart',
|
|
35
|
+
'markup-include' : 'Markup Include',
|
|
36
|
+
'Bar' : 'Bar',
|
|
37
|
+
'Line' : 'Line',
|
|
38
|
+
'Pie' : 'Pie',
|
|
39
|
+
'Spark Line' : 'Spark Line',
|
|
40
|
+
'us': 'United States (State- or County-Level)',
|
|
41
|
+
'us-county': 'United States (State- or County-Level)',
|
|
42
|
+
'world' : 'World',
|
|
43
|
+
'single-state': 'U.S. State'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const Widget = ({ data = {}, addVisualization, type }) => {
|
|
47
|
+
const { rows, visualizations, config, updateConfig } = useContext(Context)
|
|
48
|
+
|
|
49
|
+
console.log('type', type)
|
|
50
|
+
|
|
51
|
+
const handleWidgetMove = (item, monitor) => {
|
|
52
|
+
let result = monitor.getDropResult()
|
|
53
|
+
|
|
54
|
+
if(!result) return null
|
|
55
|
+
|
|
56
|
+
const { rowIdx, colIdx } = result
|
|
57
|
+
|
|
58
|
+
if(undefined !== data.rowIdx) {
|
|
59
|
+
rows[data.rowIdx][data.colIdx].widget = null // Wipe from old position
|
|
60
|
+
|
|
61
|
+
rows[rowIdx][colIdx].widget = data.uid // Add to new row and col
|
|
62
|
+
} else {
|
|
63
|
+
// Item does not exist, instantiate a new one
|
|
64
|
+
const newViz = addVisualization()
|
|
65
|
+
visualizations[newViz.uid] = newViz // Add to widgets collection
|
|
66
|
+
rows[rowIdx][colIdx].widget = newViz.uid // Store reference in rows collection under the specific column
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
updateConfig({...config, rows, visualizations})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const [ { isDragging, ...collected }, drag ] = useDrag({
|
|
73
|
+
type: 'vis-widget',
|
|
74
|
+
end: handleWidgetMove,
|
|
75
|
+
collect: (monitor) => ({
|
|
76
|
+
isDragging: monitor.isDragging()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const deleteWidget = () => {
|
|
81
|
+
rows[data.rowIdx][data.colIdx].widget = null
|
|
82
|
+
|
|
83
|
+
delete visualizations[data.uid]
|
|
84
|
+
|
|
85
|
+
updateConfig({...config, rows, visualizations})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const editWidget = () => {
|
|
89
|
+
visualizations[data.uid].editing = true;
|
|
90
|
+
|
|
91
|
+
updateConfig({...config, visualizations});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="widget" ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
|
|
96
|
+
<MoveIcon className="drag-icon" />
|
|
97
|
+
<div className="widget__content">
|
|
98
|
+
{data.rowIdx !== undefined && (
|
|
99
|
+
<div className="widget-menu">
|
|
100
|
+
<div className="widget-menu-item" onClick={editWidget}><EditIcon /></div>
|
|
101
|
+
<div className="widget-menu-item" onClick={deleteWidget}><CloseIcon /></div>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
{iconHash[type]}
|
|
105
|
+
<span>{labelHash[type]}</span>
|
|
106
|
+
{data.newViz && <span onClick={editWidget} className="config-needed">Configuration needed</span>}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default Widget
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
|
|
2
|
+
<path fill="currentColor" d="M414.8 40.79L286.8 488.8C281.9 505.8 264.2 515.6 247.2 510.8C230.2 505.9 220.4 488.2 225.2 471.2L353.2 23.21C358.1 6.216 375.8-3.624 392.8 1.232C409.8 6.087 419.6 23.8 414.8 40.79H414.8zM518.6 121.4L630.6 233.4C643.1 245.9 643.1 266.1 630.6 278.6L518.6 390.6C506.1 403.1 485.9 403.1 473.4 390.6C460.9 378.1 460.9 357.9 473.4 345.4L562.7 256L473.4 166.6C460.9 154.1 460.9 133.9 473.4 121.4C485.9 108.9 506.1 108.9 518.6 121.4V121.4zM166.6 166.6L77.25 256L166.6 345.4C179.1 357.9 179.1 378.1 166.6 390.6C154.1 403.1 133.9 403.1 121.4 390.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4L121.4 121.4C133.9 108.9 154.1 108.9 166.6 121.4C179.1 133.9 179.1 154.1 166.6 166.6V166.6z"/>
|
|
3
|
+
</svg>
|