-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor header using GravityUI #566
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,224 +5,169 @@ import {flatten, isEmpty, get, chain, compact} from 'lodash'; | |
import PropTypes from 'prop-types'; | ||
import CheckboxTree from 'react-checkbox-tree'; | ||
|
||
import Label from './label'; | ||
import {mkBrowserIcon, buildComplexId} from './utils'; | ||
import Popup from '../../popup'; | ||
import ArrayContainer from '../../../containers/array'; | ||
|
||
import 'react-checkbox-tree/lib/react-checkbox-tree.css'; | ||
import './index.styl'; | ||
|
||
const BrowserList = (props) => { | ||
const {available, onChange, selected: selectedProp} = props; | ||
|
||
const [expanded, setExpanded] = useState([]); | ||
const [selected, setSelected] = useState(buidSelectedItems()); | ||
const [elements, setElements] = useState([]); | ||
const treeDataMap = useMemo(buildTreeDataMap, [available]); | ||
const nodes = useMemo(prepareTreeData, [available]); | ||
const selectAll = useCallback(_selectAll, [setExpanded, treeDataMap]); | ||
|
||
useEffect(() => { | ||
updateLabels(); | ||
onChange && onChange(formatSelectedData(selected)); | ||
}, [selected]); | ||
|
||
function buildTreeDataMap() { | ||
return available.reduce((acc, {id: browserId, versions}) => { | ||
const hasChildren = !isEmpty(versions) && versions.length > 1; | ||
|
||
acc[buildComplexId(browserId)] = {browserId, isLeaf: !hasChildren}; | ||
|
||
if (hasChildren) { | ||
versions.forEach(version => { | ||
acc[buildComplexId(browserId, version)] = {browserId, version, isLeaf: true}; | ||
}); | ||
import { Button, Select, useSelectOptions } from '@gravity-ui/uikit'; | ||
|
||
const BrowserList = ({available, onChange, selected: selectedProp}) => { | ||
const getOptions = () => { | ||
const groups = {}; | ||
const DEFAULT_GROUP = "other"; | ||
let hasNestedOptions = false; | ||
available.forEach(({id: browserId, versions}) => { | ||
if (!versions || versions.length < 2) { | ||
groups[DEFAULT_GROUP] = groups[DEFAULT_GROUP] || []; | ||
groups[DEFAULT_GROUP].push({value: buildComplexId(browserId), | ||
content: <div className='browser-name'>{mkBrowserIcon(browserId)}{buildComplexId(browserId)}</div>}); | ||
return; | ||
} | ||
hasNestedOptions = true; | ||
versions.forEach((version) => { | ||
groups[browserId] = groups[browserId] || []; | ||
groups[browserId].push({value: buildComplexId(browserId, version), | ||
content: <div className='browser-name'>{mkBrowserIcon(browserId)}{buildComplexId(browserId, version)}</div>}); | ||
}) | ||
}); | ||
if (!hasNestedOptions) { | ||
return groups[DEFAULT_GROUP]; | ||
} | ||
else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few remarks about code style:
|
||
const optionsList = []; | ||
Object.keys(groups).forEach((name) => { | ||
optionsList.push({ | ||
label: name, | ||
options: groups[name] | ||
}) | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, there's no semicolon. And this is not the only place. Don't we have a linter rule turned on for that? Can you please check? eslint should issue warnings/errors for missing semicolons. |
||
return optionsList; | ||
} | ||
|
||
return acc; | ||
}, {}); | ||
} | ||
|
||
function buidSelectedItems() { | ||
const getMapping = () => { | ||
const mapping = {}; | ||
available.forEach(({id: browserId, versions}) => { | ||
if (!versions || !versions.length) { | ||
mapping[buildComplexId(browserId)] = {id: browserId}; | ||
return; | ||
} | ||
if (versions.length < 2) { | ||
mapping[buildComplexId(browserId)] = {id: browserId, version: versions[0]}; | ||
return; | ||
} | ||
versions.forEach((version) => { | ||
mapping[buildComplexId(browserId, version)] = {id: browserId, version}; | ||
}) | ||
}); | ||
return mapping; | ||
} | ||
const getSelected = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a descriptive&clear name either. By reading I suggest something like |
||
const selectedOptions = []; | ||
if (!selectedProp || !selectedProp.length) { | ||
return []; | ||
} | ||
|
||
const selectedItems = selectedProp.map(({id, versions}) => { | ||
if (!versions) { | ||
return []; | ||
selectedProp.forEach(({id: browserId, versions}) => { | ||
if (!versions || versions.length < 2) { | ||
selectedOptions.push(buildComplexId(browserId)); | ||
return; | ||
} | ||
|
||
const availableNode = available.find((item) => item.id === id); | ||
const shouldDrawOnlyRootNode = get(availableNode, 'versions.length', 1) === 1; | ||
|
||
if (shouldDrawOnlyRootNode) { | ||
return buildComplexId(id); | ||
} | ||
|
||
return versions.length | ||
? versions.map((version) => buildComplexId(id, version)) | ||
: availableNode.versions.map(version => buildComplexId(id, version)); | ||
versions.forEach((version) => { | ||
selectedOptions.push(buildComplexId(browserId, version)); | ||
}) | ||
}); | ||
|
||
return flatten(selectedItems); | ||
return selectedOptions; | ||
} | ||
|
||
function prepareTreeData() { | ||
const mkNode = ({browserId, version, icon}) => ({ | ||
icon, | ||
data: {browserId, version}, | ||
value: buildComplexId(browserId, version), | ||
label: mkLabel({browserId, version, elements}) | ||
}); | ||
|
||
return available.map(({id: browserId, versions}) => { | ||
const node = mkNode({browserId, icon: mkBrowserIcon(browserId)}); | ||
|
||
if (!isEmpty(versions) && versions.length > 1) { | ||
node.children = versions.map(version => mkNode({browserId, version})); | ||
const rawOptions = useMemo(getOptions, [available]); | ||
const getOptionsFrom = (optionsData) => { | ||
const allOptionsList = []; | ||
optionsData.forEach((option) => { | ||
if (option.label) { | ||
getOptionsFrom(option.options).forEach((o) => allOptionsList.push(o)); | ||
} | ||
|
||
return node; | ||
}); | ||
else { | ||
allOptionsList.push(option.value); | ||
} | ||
}) | ||
return allOptionsList; | ||
} | ||
const allOptions = useMemo(() => getOptionsFrom(rawOptions), [rawOptions]); | ||
const options = useSelectOptions({ | ||
options: rawOptions | ||
}); | ||
const mapping = useMemo(getMapping, [available]); | ||
const [selected, setSelected] = useState(getSelected()); | ||
|
||
const selectAll = () => { | ||
setSelected(allOptions); | ||
} | ||
|
||
function _selectAll() { | ||
const leaves = Object.keys(treeDataMap).filter(key => treeDataMap[key].isLeaf); | ||
const cb = selected => selected.length !== leaves.length | ||
? leaves | ||
: selected; | ||
|
||
setSelected(cb); | ||
const formatSelectedData = () => { | ||
const selectedData = {} | ||
selected.forEach((option) => { | ||
if (!mapping[option] || !mapping[option].id) return; | ||
const {id: browserId, version} = mapping[option]; | ||
selectedData[browserId] = selectedData[browserId] || []; | ||
selectedData[browserId].push(version); | ||
}) | ||
return Object.entries(selectedData).map(([id, versions]) => ({id, versions: compact(versions)})) | ||
} | ||
|
||
function mkLabel({browserId, version, elements}) { | ||
const renderFilter = () => { | ||
const allSelected = selected.length == options.length; | ||
return ( | ||
<Label | ||
treeDataMap={treeDataMap} | ||
browserId={browserId} | ||
version={version} | ||
elements={elements} | ||
setSelected={setSelected} | ||
/> | ||
); | ||
<div className='browserlist__filter'> | ||
<Button onClick={selectAll} width='max'> | ||
Select All | ||
</Button> | ||
</div> | ||
) | ||
} | ||
|
||
function buildElements() { | ||
const availableVersionsCount = available.reduce((acc, browser) => { | ||
acc[browser.id] = browser.versions ? browser.versions.length : undefined; | ||
return acc; | ||
}, {}); | ||
|
||
const selectedBrowsers = selected.reduce((acc, value) => { | ||
if (!treeDataMap[value]) { | ||
return acc; | ||
} | ||
|
||
const {browserId, version} = treeDataMap[value]; | ||
|
||
acc[browserId] = acc[browserId] || []; | ||
|
||
if (version) { | ||
acc[browserId].push(version); | ||
} | ||
|
||
return acc; | ||
}, {}); | ||
|
||
const elementsNew = []; | ||
|
||
for (const browserId in selectedBrowsers) { | ||
const versions = selectedBrowsers[browserId]; | ||
if (!versions.length || (availableVersionsCount[browserId] === versions.length)) { | ||
elementsNew.push(browserId); | ||
} else { | ||
elementsNew.push(...versions.map(version => buildComplexId(browserId, version))); | ||
} | ||
const renderOption = (option) => { | ||
const isTheOnlySelected = selected.includes(option.value) && selected.length == 1; | ||
const selectOnly = (e) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
setSelected([option.value]); | ||
} | ||
|
||
return elementsNew; | ||
} | ||
|
||
function updateLabels() { | ||
const newElements = buildElements(); | ||
|
||
if (get(elements, 'length', 0) !== 1 && get(newElements, 'length', 0) !== 1) { | ||
setElements(newElements); | ||
return; | ||
const selectExcept = (e) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
setSelected(allOptions.filter(o => o != option.value)); | ||
} | ||
|
||
const updatingValues = [ | ||
get(newElements, 'length', undefined) === 1 && newElements[0], | ||
get(elements, 'length', undefined) === 1 && elements[0] | ||
]; | ||
|
||
const nodesFlat = nodes.reduce((arr, node) => { | ||
const children = node.children || []; | ||
|
||
arr.push(node); | ||
children.forEach(child => arr.push(child)); | ||
|
||
return arr; | ||
}, []); | ||
|
||
const updatingNodes = nodesFlat.filter(node => { | ||
const {browserId, version} = node.data; | ||
|
||
return updatingValues.includes(buildComplexId(browserId, version)); | ||
}); | ||
|
||
updatingNodes.forEach(node => { | ||
const {browserId, version} = node.data; | ||
node.label = mkLabel({browserId, version, elements: newElements}); | ||
}); | ||
|
||
setElements(newElements); | ||
} | ||
|
||
function formatSelectedData(selectedItems) { | ||
return chain(selectedItems) | ||
.reduce((acc, value) => { | ||
const {browserId, version} = treeDataMap[value] || {}; | ||
|
||
if (browserId) { | ||
acc[browserId] = acc[browserId] || []; | ||
acc[browserId].push(version); | ||
} | ||
|
||
return acc; | ||
}, {}) | ||
.map((versions, id) => ({id, versions: compact(versions)})) | ||
.value(); | ||
} | ||
|
||
if (isEmpty(available)) { | ||
return (<div></div>); | ||
return ( | ||
<div className='browserlist__row'> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now when you click on only/except the width of the popup changes, but it shouldn't. |
||
<div className='browserlist__row_content'> | ||
{option.content} | ||
</div> | ||
<Button size='s' onClick={isTheOnlySelected ? selectExcept : selectOnly} className='action-button'>{isTheOnlySelected ? 'Except' : 'Only'}</Button> | ||
</div> | ||
) | ||
} | ||
|
||
useEffect(() => { | ||
onChange && onChange(formatSelectedData(selected)); | ||
}, [selected]); | ||
|
||
return ( | ||
<div className="browserlist"> | ||
<Popup | ||
target={<ArrayContainer elements={elements} placeholder="Browsers" />} | ||
action="click" | ||
> | ||
<div> | ||
<button className='rct-controls' onClick={selectAll}> | ||
Select all | ||
</button> | ||
<CheckboxTree | ||
nodes={nodes} | ||
checked={selected} | ||
expanded={expanded} | ||
onCheck={setSelected} | ||
onExpand={setExpanded} | ||
/> | ||
</div> | ||
</Popup> | ||
</div> | ||
); | ||
}; | ||
<Select | ||
disablePortal | ||
value={selected} | ||
options={options} | ||
multiple={true} | ||
hasCounter | ||
filterable | ||
renderFilter={renderFilter} | ||
renderOption={renderOption} | ||
onUpdate={setSelected} | ||
popupClassName='browserlist__popup' | ||
className='browserlist' | ||
/> | ||
) | ||
} | ||
|
||
BrowserList.propTypes = { | ||
available: PropTypes.arrayOf(PropTypes.shape({ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that
selectedProp
was here before, but I think it's bad naming too — it doesn't reflect what the variable holds at all. Let's name it something likeselectedBrowsersProp
, or you can try to come up with a better name.and maybe available -> availableBrowsersProp too. This would indicate that these props store values of similar shape.