@cdc/core 4.23.11 → 4.24.2
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/components/DataTable/DataTable.tsx +39 -10
- package/components/DataTable/components/ChartHeader.tsx +17 -5
- package/components/DataTable/components/ExpandCollapse.tsx +1 -1
- package/components/DataTable/helpers/chartCellMatrix.tsx +16 -2
- package/components/DataTable/helpers/customColumns.ts +25 -0
- package/components/DataTable/helpers/customSort.ts +9 -0
- package/components/DataTable/helpers/getChartCellValue.ts +2 -1
- package/components/DataTable/helpers/getDataSeriesColumns.ts +2 -1
- package/components/DataTable/helpers/getSeriesName.ts +15 -20
- package/components/DataTable/helpers/mapCellMatrix.tsx +4 -0
- package/components/DataTable/types/TableConfig.ts +11 -40
- package/components/EditorPanel/DataTableEditor.tsx +133 -0
- package/components/EditorPanel/Inputs.tsx +150 -0
- package/components/Filters.jsx +17 -15
- package/components/MediaControls.jsx +1 -1
- package/components/MultiSelect/MultiSelect.tsx +95 -0
- package/components/MultiSelect/index.ts +1 -0
- package/components/MultiSelect/multiselect.styles.css +50 -0
- package/components/Table/Table.tsx +23 -3
- package/components/Table/components/Cell.tsx +3 -3
- package/components/Table/components/GroupRow.tsx +6 -2
- package/components/Table/components/Row.tsx +9 -2
- package/components/Table/types/RowType.ts +5 -0
- package/components/_stories/DataTable.stories.tsx +41 -0
- package/components/_stories/EditorPanel.stories.tsx +53 -0
- package/components/_stories/Inputs.stories.tsx +37 -0
- package/components/_stories/MultiSelect.stories.tsx +24 -0
- package/components/_stories/_mocks/row_type.json +42 -0
- package/components/inputs/{InputSelect.jsx → InputSelect.tsx} +15 -5
- package/components/managers/DataDesigner.tsx +8 -8
- package/components/ui/{Icon.jsx → Icon.tsx} +3 -3
- package/components/ui/_stories/Colors.stories.tsx +92 -0
- package/components/ui/_stories/Icon.stories.tsx +17 -10
- package/helpers/DataTransform.ts +30 -2
- package/helpers/fetchRemoteData.js +5 -5
- package/helpers/getFileExtension.ts +28 -5
- package/helpers/getViewport.ts +23 -0
- package/helpers/isSolr.js +13 -0
- package/helpers/withDevTools.ts +50 -0
- package/package.json +2 -2
- package/styles/_data-table.scss +2 -5
- package/styles/_global-variables.scss +75 -0
- package/styles/base.scss +89 -69
- package/types/Action.ts +1 -0
- package/types/Axis.ts +39 -2
- package/types/BoxPlot.ts +21 -0
- package/types/Column.ts +16 -0
- package/types/FilterBehavior.ts +1 -0
- package/types/General.ts +9 -0
- package/types/Runtime.ts +21 -1
- package/types/Series.ts +1 -1
- package/types/Table.ts +21 -0
- package/types/UpdateFieldFunc.ts +1 -0
- package/types/ViewPort.ts +2 -0
- package/types/Visualization.ts +15 -9
- package/types/WCMSProps.ts +11 -0
- package/helpers/getViewport.js +0 -21
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { memo, useEffect, useState } from 'react'
|
|
2
|
+
import { useDebounce } from 'use-debounce'
|
|
3
|
+
|
|
4
|
+
export type Input = {
|
|
5
|
+
label: string
|
|
6
|
+
tooltip?: any
|
|
7
|
+
section?: any
|
|
8
|
+
placeholder?: string
|
|
9
|
+
subsection?: any
|
|
10
|
+
updateField?: Function
|
|
11
|
+
fieldName?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TextFieldProps = {
|
|
15
|
+
className?: string
|
|
16
|
+
value: string | number
|
|
17
|
+
type?: 'text' | 'number' | 'textarea' | 'date'
|
|
18
|
+
min?: number
|
|
19
|
+
max?: number
|
|
20
|
+
i?: number
|
|
21
|
+
id?: string
|
|
22
|
+
} & Input
|
|
23
|
+
|
|
24
|
+
export type CheckboxProps = {
|
|
25
|
+
value?: boolean
|
|
26
|
+
min?: number
|
|
27
|
+
i?: number
|
|
28
|
+
className?: string
|
|
29
|
+
} & Input
|
|
30
|
+
|
|
31
|
+
export type SelectProps = {
|
|
32
|
+
value?: string
|
|
33
|
+
options?: string[]
|
|
34
|
+
required?: boolean
|
|
35
|
+
initial?: string
|
|
36
|
+
|
|
37
|
+
// all other props
|
|
38
|
+
[x: string]: any
|
|
39
|
+
} & Input
|
|
40
|
+
|
|
41
|
+
const TextField = memo((props: TextFieldProps) => {
|
|
42
|
+
const { label, tooltip, section = null, subsection = null, fieldName, updateField, value: stateValue, type = 'text', i = null, min = null, ...attributes } = props
|
|
43
|
+
const [value, setValue] = useState(stateValue)
|
|
44
|
+
const [debouncedValue] = useDebounce(value, 500)
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
|
|
48
|
+
updateField(section, subsection, fieldName, debouncedValue, i)
|
|
49
|
+
}
|
|
50
|
+
}, [debouncedValue])
|
|
51
|
+
|
|
52
|
+
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
|
|
53
|
+
|
|
54
|
+
const onChange = e => {
|
|
55
|
+
if ('number' !== type || min === null) {
|
|
56
|
+
setValue(e.target.value)
|
|
57
|
+
} else {
|
|
58
|
+
if (!e.target.value || min <= parseFloat(e.target.value)) {
|
|
59
|
+
setValue(e.target.value)
|
|
60
|
+
} else {
|
|
61
|
+
setValue(min.toString())
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let formElement = <input type='text' name={name} onChange={onChange} {...attributes} value={value} />
|
|
67
|
+
|
|
68
|
+
if ('textarea' === type) {
|
|
69
|
+
formElement = <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if ('number' === type) {
|
|
73
|
+
formElement = <input type='number' name={name} onChange={onChange} {...attributes} value={value} />
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if ('date' === type) {
|
|
77
|
+
formElement = <input type='date' name={name} onChange={onChange} {...attributes} value={value} />
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<label>
|
|
82
|
+
<span className='edit-label column-heading'>
|
|
83
|
+
{label}
|
|
84
|
+
{tooltip}
|
|
85
|
+
</span>
|
|
86
|
+
{formElement}
|
|
87
|
+
</label>
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const CheckBox = memo((props: CheckboxProps) => {
|
|
92
|
+
const { label, value, fieldName, section = null, subsection = null, tooltip, updateField, ...attributes } = props
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<label className='checkbox column-heading'>
|
|
96
|
+
<input
|
|
97
|
+
type='checkbox'
|
|
98
|
+
name={fieldName}
|
|
99
|
+
checked={value}
|
|
100
|
+
onChange={e => {
|
|
101
|
+
updateField(section, subsection, fieldName, !value)
|
|
102
|
+
}}
|
|
103
|
+
{...attributes}
|
|
104
|
+
/>
|
|
105
|
+
<span className='edit-label'>
|
|
106
|
+
{label}
|
|
107
|
+
{tooltip}
|
|
108
|
+
</span>
|
|
109
|
+
</label>
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const Select = memo((props: SelectProps) => {
|
|
114
|
+
const { label, value, options, fieldName, section = null, subsection = null, required = false, tooltip, updateField, initial: initialValue, ...attributes } = props
|
|
115
|
+
let optionsJsx = options.map((optionName, index) => (
|
|
116
|
+
<option value={optionName} key={index}>
|
|
117
|
+
{optionName}
|
|
118
|
+
</option>
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
if (initialValue) {
|
|
122
|
+
optionsJsx.unshift(
|
|
123
|
+
<option value='' key='initial'>
|
|
124
|
+
{initialValue}
|
|
125
|
+
</option>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<label>
|
|
131
|
+
<span className='edit-label'>
|
|
132
|
+
{label}
|
|
133
|
+
{tooltip}
|
|
134
|
+
</span>
|
|
135
|
+
<select
|
|
136
|
+
className={required && !value ? 'warning' : ''}
|
|
137
|
+
name={fieldName}
|
|
138
|
+
value={value}
|
|
139
|
+
onChange={event => {
|
|
140
|
+
updateField(section, subsection, fieldName, event.target.value)
|
|
141
|
+
}}
|
|
142
|
+
{...attributes}
|
|
143
|
+
>
|
|
144
|
+
{optionsJsx}
|
|
145
|
+
</select>
|
|
146
|
+
</label>
|
|
147
|
+
)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
export { Select, CheckBox, TextField }
|
package/components/Filters.jsx
CHANGED
|
@@ -74,22 +74,17 @@ export const useFilters = props => {
|
|
|
74
74
|
const changeFilterActive = (index, value) => {
|
|
75
75
|
let newFilters = visualizationConfig.type === 'map' ? [...filteredData] : [...visualizationConfig.filters]
|
|
76
76
|
|
|
77
|
-
newFilters[index].active = value
|
|
78
|
-
setConfig({ ...visualizationConfig })
|
|
79
|
-
|
|
80
|
-
// If this is a button filter type show the button.
|
|
81
77
|
if (visualizationConfig.filterBehavior === 'Apply Button') {
|
|
78
|
+
newFilters[index].queuedActive = value
|
|
82
79
|
setShowApplyButton(true)
|
|
80
|
+
} else {
|
|
81
|
+
newFilters[index].active = value
|
|
83
82
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
filters: newFilters
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
|
|
83
|
+
setConfig({
|
|
84
|
+
...visualizationConfig,
|
|
85
|
+
filters: newFilters
|
|
86
|
+
})
|
|
87
|
+
|
|
93
88
|
// Used for setting active filter, fromHash breaks the filteredData functionality.
|
|
94
89
|
if (visualizationConfig.type === 'map' && visualizationConfig.filterBehavior === 'Filter Change') {
|
|
95
90
|
setFilteredData(newFilters)
|
|
@@ -102,6 +97,13 @@ export const useFilters = props => {
|
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
const handleApplyButton = newFilters => {
|
|
100
|
+
newFilters.forEach(newFilter => {
|
|
101
|
+
if(newFilter.queuedActive){
|
|
102
|
+
newFilter.active = newFilter.queuedActive
|
|
103
|
+
delete newFilter.queuedActive
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
105
107
|
setConfig({ ...visualizationConfig, filters: newFilters })
|
|
106
108
|
|
|
107
109
|
if (type === 'map') {
|
|
@@ -320,7 +322,7 @@ const Filters = props => {
|
|
|
320
322
|
const tabValues = []
|
|
321
323
|
const tabBarValues = []
|
|
322
324
|
|
|
323
|
-
const { active, label, filterStyle } = singleFilter
|
|
325
|
+
const { active, queuedActive, label, filterStyle } = singleFilter
|
|
324
326
|
|
|
325
327
|
handleSorting(singleFilter)
|
|
326
328
|
|
|
@@ -387,7 +389,7 @@ const Filters = props => {
|
|
|
387
389
|
{filterStyle === 'tab' && !mobileFilterStyle && <Filters.Tabs tabs={tabValues} />}
|
|
388
390
|
{filterStyle === 'pill' && !mobileFilterStyle && <Filters.Pills pills={pillValues} />}
|
|
389
391
|
{filterStyle === 'tab bar' && !mobileFilterStyle && <Filters.TabBar filter={singleFilter} index={outerIndex} />}
|
|
390
|
-
{(filterStyle === 'dropdown' || mobileFilterStyle) && <Filters.Dropdown filter={singleFilter} index={outerIndex} label={label} active={active} filters={values} />}
|
|
392
|
+
{(filterStyle === 'dropdown' || mobileFilterStyle) && <Filters.Dropdown filter={singleFilter} index={outerIndex} label={label} active={queuedActive || active} filters={values} />}
|
|
391
393
|
</>
|
|
392
394
|
</div>
|
|
393
395
|
)
|
|
@@ -59,7 +59,7 @@ const generateMedia = (state, type, elementToCapture) => {
|
|
|
59
59
|
|
|
60
60
|
switch (type) {
|
|
61
61
|
case 'image':
|
|
62
|
-
html2canvas(baseSvg, {foreignObjectRendering: true}).then(canvas => {
|
|
62
|
+
html2canvas(baseSvg, {foreignObjectRendering: true, x: -1 * (window.pageXOffset + baseSvg.getBoundingClientRect().left), y: -1 * (window.pageYOffset + baseSvg.getBoundingClientRect().top)}).then(canvas => {
|
|
63
63
|
saveImageAs(canvas.toDataURL(), filename + '.png')
|
|
64
64
|
})
|
|
65
65
|
return
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import Icon from '../ui/Icon'
|
|
3
|
+
|
|
4
|
+
import './multiselect.styles.css'
|
|
5
|
+
import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
|
|
6
|
+
|
|
7
|
+
interface Option {
|
|
8
|
+
value: string
|
|
9
|
+
label: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MultiSelectProps {
|
|
13
|
+
section?: string
|
|
14
|
+
subsection?: string
|
|
15
|
+
fieldName: string
|
|
16
|
+
options: Option[]
|
|
17
|
+
updateField: UpdateFieldFunc<string[]>
|
|
18
|
+
label?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField }) => {
|
|
22
|
+
const [selectedItems, setSelectedItems] = useState<Option[]>([])
|
|
23
|
+
const [expanded, setExpanded] = useState(false)
|
|
24
|
+
const multiSelectRef = useRef(null)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleClickOutside = event => {
|
|
28
|
+
if (multiSelectRef.current && !multiSelectRef.current.contains(event.target)) {
|
|
29
|
+
setExpanded(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
37
|
+
}
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const update = newItems =>
|
|
41
|
+
updateField(
|
|
42
|
+
section,
|
|
43
|
+
subsection,
|
|
44
|
+
fieldName,
|
|
45
|
+
newItems.map(item => item.value)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const handleItemSelect = (option: Option) => {
|
|
49
|
+
const newItems = [...selectedItems, option]
|
|
50
|
+
setSelectedItems(newItems)
|
|
51
|
+
update(newItems)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handleItemRemove = (option: Option) => {
|
|
55
|
+
const newItems = selectedItems.filter(item => item.value !== option.value)
|
|
56
|
+
setSelectedItems(newItems)
|
|
57
|
+
update(newItems)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const multiID = 'multiSelect_' + label
|
|
61
|
+
return (
|
|
62
|
+
<div ref={multiSelectRef} className='cove-multiselect'>
|
|
63
|
+
{label && (
|
|
64
|
+
<span id={multiID} className='edit-label cove-input__label'>
|
|
65
|
+
{label}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<div aria-labelledby={label ? multiID : undefined} className='selected'>
|
|
70
|
+
{selectedItems.map(item => (
|
|
71
|
+
<div key={item.value} role='button' tabIndex={0} onClick={() => handleItemRemove(item)} onKeyUp={() => handleItemRemove(item)}>
|
|
72
|
+
{item.label}
|
|
73
|
+
<button aria-label='Remove' onClick={() => handleItemRemove(item)}>
|
|
74
|
+
x
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
<button aria-label={expanded ? 'Collapse' : 'Expand'} className='expand' onClick={() => setExpanded(!expanded)}>
|
|
79
|
+
<Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<ul className={'dropdown' + (expanded ? '' : ' hide')}>
|
|
83
|
+
{options
|
|
84
|
+
.filter(option => !selectedItems.find(item => item.value === option.value))
|
|
85
|
+
.map(option => (
|
|
86
|
+
<li className='cove-multiselect-li' key={option.value} role='option' tabIndex={0} onClick={() => handleItemSelect(option)} onKeyUp={() => handleItemSelect(option)}>
|
|
87
|
+
{option.label}
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default MultiSelect
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './MultiSelect'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.cove-multiselect {
|
|
2
|
+
position: relative;
|
|
3
|
+
.selected {
|
|
4
|
+
border: 1px solid #ccc;
|
|
5
|
+
padding: 5px;
|
|
6
|
+
min-height: 40px;
|
|
7
|
+
:is(button) {
|
|
8
|
+
border: none;
|
|
9
|
+
background: none;
|
|
10
|
+
}
|
|
11
|
+
:is(div) {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
padding: 0 0 0 5px;
|
|
14
|
+
margin-right: 5px;
|
|
15
|
+
margin-bottom: 2px;
|
|
16
|
+
background: #ccc;
|
|
17
|
+
border-radius: 5px;
|
|
18
|
+
}
|
|
19
|
+
.expand {
|
|
20
|
+
padding: 0 5px;
|
|
21
|
+
border-radius: 5px;
|
|
22
|
+
background: #ccc;
|
|
23
|
+
float: right;
|
|
24
|
+
}
|
|
25
|
+
border-radius: 5px;
|
|
26
|
+
}
|
|
27
|
+
.dropdown {
|
|
28
|
+
background: white;
|
|
29
|
+
position: absolute;
|
|
30
|
+
margin-top: 5px;
|
|
31
|
+
border: 1px solid #ccc;
|
|
32
|
+
padding: 0;
|
|
33
|
+
min-height: 40px;
|
|
34
|
+
overflow: scroll;
|
|
35
|
+
max-height: 200px;
|
|
36
|
+
z-index: 1;
|
|
37
|
+
&.hide {
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:is(li) {
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
list-style: none;
|
|
44
|
+
padding-left: 10px;
|
|
45
|
+
&:hover {
|
|
46
|
+
background: #ccc;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -2,6 +2,7 @@ import { ReactNode } from 'react'
|
|
|
2
2
|
import Row from './components/Row'
|
|
3
3
|
import GroupRow from './components/GroupRow'
|
|
4
4
|
import { CellMatrix, GroupCellMatrix } from './types/CellMatrix'
|
|
5
|
+
import { RowType } from './types/RowType'
|
|
5
6
|
|
|
6
7
|
type TableProps = {
|
|
7
8
|
childrenMatrix: CellMatrix | GroupCellMatrix
|
|
@@ -14,12 +15,15 @@ type TableProps = {
|
|
|
14
15
|
'aria-live'?: 'off' | 'assertive' | 'polite'
|
|
15
16
|
hidden?: boolean
|
|
16
17
|
'aria-rowcount'?: number
|
|
18
|
+
cellMinWidth?: number
|
|
17
19
|
}
|
|
20
|
+
wrapColumns?: boolean
|
|
21
|
+
hasRowType?: boolean // if it has row type then the first column is the row type which will explain how to render the row
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
type Position = 'sticky'
|
|
21
25
|
|
|
22
|
-
const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent, tableOptions }: TableProps) => {
|
|
26
|
+
const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent, tableOptions, wrapColumns, hasRowType }: TableProps) => {
|
|
23
27
|
const headStyle = stickyHeader ? { position: 'sticky' as Position, top: 0, zIndex: 999 } : {}
|
|
24
28
|
const isGroupedMatrix = !Array.isArray(childrenMatrix)
|
|
25
29
|
return (
|
|
@@ -33,13 +37,29 @@ const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent,
|
|
|
33
37
|
const rows = childrenMatrix[groupName].map((row, i) => {
|
|
34
38
|
colSpan = row.length
|
|
35
39
|
const key = `${tableName}-${groupName}-row-${i}`
|
|
36
|
-
return <Row key={key} rowKey={key} childRow={row} />
|
|
40
|
+
return <Row key={key} rowKey={key} childRow={row} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
37
41
|
})
|
|
38
42
|
return [<GroupRow label={groupName} colSpan={colSpan} key={`${tableName}-${groupName}`} />, ...rows]
|
|
39
43
|
})
|
|
40
44
|
: childrenMatrix.map((childRow, i) => {
|
|
45
|
+
let childRowCopy = [...childRow]
|
|
46
|
+
let rowType = undefined
|
|
47
|
+
if (hasRowType) rowType = childRowCopy.shift()
|
|
41
48
|
const key = `${tableName}-row-${i}`
|
|
42
|
-
|
|
49
|
+
if (rowType === undefined) {
|
|
50
|
+
return <Row key={key} rowKey={key} childRow={childRow} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
51
|
+
} else {
|
|
52
|
+
switch (rowType) {
|
|
53
|
+
case RowType.row_group:
|
|
54
|
+
return <GroupRow label={childRowCopy[0]} colSpan={childRowCopy.length} key={key} />
|
|
55
|
+
case RowType.total:
|
|
56
|
+
return <Row key={key} rowKey={key} childRow={childRowCopy} isTotal={true} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
57
|
+
case RowType.row_group_total:
|
|
58
|
+
return <GroupRow label={childRowCopy[0]} colSpan={1} key={key} data={childRowCopy.slice(1)} />
|
|
59
|
+
default:
|
|
60
|
+
return <Row key={key} rowKey={key} childRow={childRowCopy} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
})}
|
|
44
64
|
</tbody>
|
|
45
65
|
</table>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const Cell = ({ children }) => {
|
|
1
|
+
const Cell = ({ children, style, isBold = false }) => {
|
|
2
2
|
return (
|
|
3
|
-
<td tabIndex={0} role='gridcell'>
|
|
4
|
-
{children}
|
|
3
|
+
<td tabIndex={0} role='gridcell' style={style}>
|
|
4
|
+
{isBold ? <strong>{children}</strong> : children}
|
|
5
5
|
</td>
|
|
6
6
|
)
|
|
7
7
|
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
|
|
1
3
|
type GroupRowProps = {
|
|
2
|
-
label:
|
|
4
|
+
label: ReactNode
|
|
3
5
|
colSpan: number
|
|
6
|
+
data?: ReactNode[]
|
|
4
7
|
}
|
|
5
8
|
|
|
6
|
-
const GroupRow = ({ label, colSpan }: GroupRowProps) => {
|
|
9
|
+
const GroupRow = ({ label, colSpan, data }: GroupRowProps) => {
|
|
7
10
|
return (
|
|
8
11
|
<tr>
|
|
9
12
|
<th scope='colgroup' colSpan={colSpan}>
|
|
10
13
|
{label}
|
|
11
14
|
</th>
|
|
15
|
+
{data && data.map((item, i) => <th key={`${label}-${i}`}>{item}</th>)}
|
|
12
16
|
</tr>
|
|
13
17
|
)
|
|
14
18
|
}
|
|
@@ -4,13 +4,20 @@ import Cell from './Cell'
|
|
|
4
4
|
type RowProps = {
|
|
5
5
|
childRow: ReactNode[]
|
|
6
6
|
rowKey: string
|
|
7
|
+
wrapColumns: boolean
|
|
8
|
+
isTotal?: boolean
|
|
9
|
+
cellMinWidth?: number
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
const Row = ({ childRow, rowKey }: RowProps) => {
|
|
12
|
+
const Row = ({ childRow, rowKey, wrapColumns, cellMinWidth = 0, isTotal }: RowProps) => {
|
|
13
|
+
const whiteSpace = wrapColumns ? 'unset' : 'nowrap'
|
|
14
|
+
const minWidth = cellMinWidth + 'px'
|
|
10
15
|
return (
|
|
11
16
|
<tr>
|
|
12
17
|
{childRow.map((child, i) => (
|
|
13
|
-
<Cell key={rowKey + '__' + i}
|
|
18
|
+
<Cell key={rowKey + '__' + i} style={{ whiteSpace, minWidth }} isBold={isTotal}>
|
|
19
|
+
{child}
|
|
20
|
+
</Cell>
|
|
14
21
|
))}
|
|
15
22
|
</tr>
|
|
16
23
|
)
|
|
@@ -5,6 +5,8 @@ import './styles.scss'
|
|
|
5
5
|
import Example_1 from './_mocks/dashboard_no_filter.json'
|
|
6
6
|
import CityStateExample from './_mocks/example-city-state.json'
|
|
7
7
|
import { displayGeoName } from '@cdc/map/src/helpers/displayGeoName'
|
|
8
|
+
import rowTypeData from './_mocks/row_type.json'
|
|
9
|
+
import { TableConfig } from '../DataTable/types/TableConfig'
|
|
8
10
|
|
|
9
11
|
const meta: Meta<typeof DataTable> = {
|
|
10
12
|
title: 'Components/Organisms/DataTable',
|
|
@@ -60,3 +62,42 @@ export const Grouped: Story = {
|
|
|
60
62
|
tabbingId: datasetKey
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
|
|
66
|
+
export const RowType: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
config: {
|
|
69
|
+
dashboard: {
|
|
70
|
+
theme: 'theme-blue',
|
|
71
|
+
title: 'RowType'
|
|
72
|
+
},
|
|
73
|
+
title: 'RowType',
|
|
74
|
+
dataUrl: '/examples/feature/__data__/ardi.json',
|
|
75
|
+
animate: false,
|
|
76
|
+
animateReplay: true,
|
|
77
|
+
palette: 'qualitative-soft',
|
|
78
|
+
aspectRatio: 1,
|
|
79
|
+
dataFormat: {
|
|
80
|
+
roundTo: 1,
|
|
81
|
+
commas: false,
|
|
82
|
+
prefix: '',
|
|
83
|
+
suffix: ''
|
|
84
|
+
},
|
|
85
|
+
legend: {
|
|
86
|
+
hide: false
|
|
87
|
+
},
|
|
88
|
+
table: {
|
|
89
|
+
label: 'Data Table',
|
|
90
|
+
expanded: true,
|
|
91
|
+
show: true,
|
|
92
|
+
customTableConfig: true
|
|
93
|
+
}
|
|
94
|
+
} as unknown as TableConfig,
|
|
95
|
+
dataConfig: { data: rowTypeData },
|
|
96
|
+
rawData: rowTypeData,
|
|
97
|
+
runtimeData: rowTypeData,
|
|
98
|
+
expandDataTable: true,
|
|
99
|
+
tableTitle: 'DataTable',
|
|
100
|
+
viewport: 'lg',
|
|
101
|
+
tabbingId: '#asdf'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import DataTableEditor from '../EditorPanel/DataTableEditor'
|
|
4
|
+
import { Accordion, AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
|
|
7
|
+
const EditorPanel = () => {
|
|
8
|
+
const { config, isDashboard } = Primary.args
|
|
9
|
+
const [_config, setConfig] = useState(config)
|
|
10
|
+
const updateField = (section, subsection, fieldName, value) => {
|
|
11
|
+
setConfig({
|
|
12
|
+
..._config,
|
|
13
|
+
[section]: {
|
|
14
|
+
..._config[section],
|
|
15
|
+
[fieldName]: value
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
<Accordion>
|
|
21
|
+
<AccordionItem>
|
|
22
|
+
<AccordionItemHeading>
|
|
23
|
+
<AccordionItemButton>Data Table</AccordionItemButton>
|
|
24
|
+
</AccordionItemHeading>
|
|
25
|
+
<AccordionItemPanel>
|
|
26
|
+
<DataTableEditor config={_config} isDashboard={isDashboard} updateField={updateField} isLoadedFromUrl={false} />
|
|
27
|
+
</AccordionItemPanel>
|
|
28
|
+
</AccordionItem>
|
|
29
|
+
</Accordion>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const meta: Meta<typeof DataTableEditor> = {
|
|
34
|
+
title: 'Components/Organisms/EditorPanel',
|
|
35
|
+
component: EditorPanel
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default meta
|
|
39
|
+
|
|
40
|
+
type Story = StoryObj<typeof DataTableEditor>
|
|
41
|
+
|
|
42
|
+
export const Primary: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
config: {
|
|
45
|
+
table: {
|
|
46
|
+
label: 'Data Table',
|
|
47
|
+
show: true
|
|
48
|
+
},
|
|
49
|
+
visualizationType: 'Pie'
|
|
50
|
+
},
|
|
51
|
+
isDashboard: true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import InputSelect from '../inputs/InputSelect'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
|
|
6
|
+
const Inputs: React.FC = ({ config }: any) => {
|
|
7
|
+
const [_config, setConfig] = useState(config)
|
|
8
|
+
const updateField = (section, subsection, fieldName, value) => {
|
|
9
|
+
setConfig({
|
|
10
|
+
..._config,
|
|
11
|
+
[section]: {
|
|
12
|
+
..._config[section],
|
|
13
|
+
[fieldName]: value
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<InputSelect label='Select' options={['apple', 'banana', 'orange']} fieldName='inputselect' updateField={updateField} />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const meta: Meta<typeof Inputs> = {
|
|
25
|
+
title: 'Components/Atoms/Inputs',
|
|
26
|
+
component: Inputs
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default meta
|
|
30
|
+
|
|
31
|
+
type Story = StoryObj<typeof Inputs>
|
|
32
|
+
|
|
33
|
+
export const Select: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
config: {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import MultiSelect from '../MultiSelect'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MultiSelect> = {
|
|
6
|
+
title: 'Components/Molecules/MultiSelect',
|
|
7
|
+
component: MultiSelect
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Story = StoryObj<typeof MultiSelect>
|
|
11
|
+
|
|
12
|
+
export const Primary: Story = {
|
|
13
|
+
args: {
|
|
14
|
+
options: [
|
|
15
|
+
{ value: '1', label: 'One' },
|
|
16
|
+
{ value: '2', label: 'Two' },
|
|
17
|
+
{ value: '3', label: 'Three' }
|
|
18
|
+
],
|
|
19
|
+
label: 'MultiSelect',
|
|
20
|
+
updateField: (section, subsection, fieldName, value) => {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|