Last active
April 30, 2024 13:30
-
-
Save AgrYpn1a/78c033ab4054d9073dfa1bf813508dd1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Tree, TreeItem, TreeNode } from './types'; | |
export const treeFind = <T extends TreeItem>(matchId: string, tree: Tree<T>): TreeNode<T> | undefined => { | |
return tree.reduce( | |
(result: TreeNode<T> | undefined, curr) => | |
curr.item.id === matchId ? curr : result?.item.id === matchId ? result : treeFind(matchId, curr.children || []), | |
undefined, | |
); | |
}; | |
export function treeMap<T extends TreeItem>(tree: Tree<T>, functor: (node: TreeNode<T>) => TreeNode<T>) { | |
return tree.map((node) => ({ ...functor(node), ...(node.children && { children: treeMap(node.children, functor) }) })); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import Box from '@mui/material/Box'; | |
import { TreeItem as MuiTreeItem, TreeItemProps, treeItemClasses } from '@mui/x-tree-view/TreeItem'; | |
import Typography from '@mui/material/Typography'; | |
import { styled, useTheme } from '@mui/material/styles'; | |
import { defaultStyledOptions } from '@protecht/ui-library/library/utils/defaultStyledOptions'; | |
import { faTriangle } from '@fortawesome/pro-solid-svg-icons'; | |
import { faTriangle as faTriangleRegular } from '@fortawesome/pro-regular-svg-icons'; | |
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | |
export type StyledTreeItemProps = TreeItemProps & { | |
labelIcon: React.ReactNode; | |
labelInfo?: string; | |
labelText: string; | |
indentation?: number; | |
OverrideLabel?: React.ReactNode; | |
}; | |
export const StyledTreeItemRoot = styled( | |
MuiTreeItem, | |
defaultStyledOptions, | |
)<{ $indentation: number }>(({ theme, $indentation }) => ({ | |
color: theme.palette.text.secondary, | |
minWidth: '100%', | |
width: 'max-content', | |
'& .MuiTreeItem-root': { | |
width: '100%', | |
'& .MuiTreeItem-content:hover': { | |
background: 'linear-gradient(rgba(0, 0, 0, 0.15) 0 0)', | |
'& .MuiTreeItem-content:hover': { | |
background: 'none', | |
}, | |
}, | |
'& .MuiTreeItem-content.Mui-selected': { | |
background: 'linear-gradient(rgba(0, 0, 0, 0.30) 0 0)', | |
'& .MuiTreeItem-content.Mui-selected': { | |
background: 'none', | |
}, | |
}, | |
}, | |
'& .MuiTreeItem-label': { | |
width: '100%', | |
}, | |
[`& .${treeItemClasses.group}`]: { | |
width: '100%', | |
}, | |
'& .MuiCollapse-root': { | |
margin: 0, | |
width: '100%', | |
}, | |
'& .MuiCollapse-wrapperInner .MuiTreeItem-content': { | |
width: '100%', | |
paddingLeft: `${$indentation}px`, | |
}, | |
})); | |
const TreeViewItem = React.forwardRef(function StyledTreeItem(props: StyledTreeItemProps, ref: React.Ref<HTMLLIElement>) { | |
const { OverrideLabel, labelIcon: LabelIcon, labelInfo, labelText, indentation = 16, ...other } = props; | |
const theme = useTheme(); | |
return ( | |
<StyledTreeItemRoot | |
ref={ref} | |
$indentation={indentation} | |
collapseIcon={ | |
<FontAwesomeIcon | |
aria-label="triangle-down" | |
aria-hidden={false} | |
icon={faTriangle} | |
className="fa-rotate-180" | |
style={{ | |
fontSize: '12px', | |
color: theme.palette.text.primary, | |
}} | |
/> | |
} | |
expandIcon={ | |
<FontAwesomeIcon | |
aria-label="triangle-right" | |
aria-hidden={false} | |
icon={faTriangleRegular} | |
className="fa-rotate-90" | |
style={{ | |
fontSize: '12px', | |
color: theme.palette.text.primary, | |
}} | |
/> | |
} | |
label={ | |
OverrideLabel ?? ( | |
<Box | |
sx={{ | |
display: 'flex', | |
alignItems: 'center', | |
p: 0.5, | |
pr: 0, | |
}} | |
> | |
<Box | |
color="inherit" | |
sx={{ mr: 1 }} | |
> | |
{LabelIcon} | |
</Box> | |
<Typography | |
variant="body2" | |
sx={{ fontWeight: 'inherit', flexGrow: 1 }} | |
> | |
{labelText} | |
</Typography> | |
<Typography | |
variant="caption" | |
color="inherit" | |
> | |
{labelInfo} | |
</Typography> | |
</Box> | |
) | |
} | |
{...other} | |
/> | |
); | |
}); | |
export default TreeViewItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useCallback, useMemo, useState } from 'react'; | |
import { TreeView as MuiTreeView } from '@mui/x-tree-view/TreeView'; | |
import Grid from '@mui/material/Grid'; | |
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | |
import { faFolder, faFolderOpen, faTag } from '@fortawesome/pro-regular-svg-icons'; | |
import { faTriangle } from '@fortawesome/pro-solid-svg-icons'; | |
import { faTriangle as faTriangleRegular } from '@fortawesome/pro-regular-svg-icons'; | |
import { styled, useTheme } from '@mui/material/styles'; | |
import Box from '@mui/material/Box'; | |
import Loading from '@protecht/ui-library/library/components/Loading'; | |
import TreeViewItem from './TreeItem'; | |
import { TreeItem, TreeNode } from 'lib/tree/types'; | |
import { treeFind, treeMap } from 'lib/tree/functions'; | |
type TreeViewProps<T extends TreeItem> = { | |
items: TreeNode<T>[]; | |
onChange: (value: T) => void; | |
onItemDoubleClick?: (value: T) => void; | |
indentation?: number; | |
loading?: boolean; | |
renderTreeOverride?: React.FC<TreeNode<T>>; | |
TreeNodeLabelOverride?: React.FC<T>; | |
TreeLeafLabelOverride?: React.FC<T>; | |
highlightedItemsIds?: (string | number)[]; | |
disableInteraction?: boolean; | |
}; | |
function TreeView<T extends TreeItem>({ | |
items, | |
onChange, | |
onItemDoubleClick, | |
indentation, | |
loading, | |
renderTreeOverride, | |
TreeNodeLabelOverride, | |
TreeLeafLabelOverride, | |
highlightedItemsIds, | |
disableInteraction, | |
}: TreeViewProps<T>) { | |
const theme = useTheme(); | |
const [expanded, setExpanded] = useState<(string | number)[]>([]); | |
const transformedItems = useMemo( | |
() => | |
treeMap(items, (node) => ({ | |
...node, | |
item: { ...node.item, isHighlighted: highlightedItemsIds?.includes(node.item.id) }, | |
expanded: expanded.includes(node.item.id), | |
})), | |
[items, expanded, highlightedItemsIds], | |
); | |
const renderTree = useCallback( | |
(node: TreeNode<T>) => { | |
if (!node.children) { | |
return ( | |
<TreeViewItem | |
key={node.item.id} | |
nodeId={node.item.id.toString()} | |
labelText={node.item.label} | |
labelIcon={ | |
<LabelIconWrapper> | |
<FontAwesomeIcon | |
icon={faTag} | |
color={theme.palette.primary.main} | |
/> | |
</LabelIconWrapper> | |
} | |
indentation={indentation} | |
OverrideLabel={TreeLeafLabelOverride && <TreeLeafLabelOverride {...node.item} />} | |
/> | |
); | |
} else { | |
return ( | |
<TreeViewItem | |
key={node.item.id} | |
nodeId={node.item.id.toString()} | |
labelText={node.item.label} | |
labelIcon={ | |
<LabelIconWrapper> | |
{expanded.some((expItemId) => expItemId === node.item.id) ? ( | |
<FontAwesomeIcon | |
icon={faFolderOpen} | |
color={theme.palette.primary.main} | |
/> | |
) : ( | |
<FontAwesomeIcon | |
icon={faFolder} | |
color={theme.palette.primary.main} | |
/> | |
)} | |
</LabelIconWrapper> | |
} | |
indentation={indentation} | |
OverrideLabel={TreeNodeLabelOverride && <TreeNodeLabelOverride {...node.item} />} | |
> | |
{node.children?.map(renderTree)} | |
</TreeViewItem> | |
); | |
} | |
}, | |
[expanded, indentation, theme, TreeNodeLabelOverride, TreeLeafLabelOverride], | |
); | |
const handleOnNodeSelect = useCallback( | |
(event: React.MouseEvent<HTMLElement>, nodeId) => { | |
const resultNode = treeFind(nodeId, items); | |
if (resultNode) { | |
// Process double-click event | |
if (event.detail === 2) { | |
onItemDoubleClick?.(resultNode.item); | |
} | |
onChange(resultNode.item); | |
} | |
}, | |
[items, onChange, onItemDoubleClick], | |
); | |
return ( | |
<Grid | |
container | |
direction="column" | |
sx={{ | |
margin: '0 auto', | |
flexWrap: 'nowrap', | |
height: 0, | |
flexGrow: 1, | |
overflowY: 'auto', | |
'*': { | |
userSelect: 'none', | |
...(disableInteraction && { | |
pointerEvents: 'none', | |
}), | |
}, | |
}} | |
> | |
<BodyGrid item> | |
{loading ? ( | |
<Box | |
sx={{ | |
width: '100%', | |
height: '100%', | |
position: 'relative', | |
}} | |
> | |
<Loading /> | |
</Box> | |
) : ( | |
<MuiTreeView | |
defaultCollapseIcon={ | |
<FontAwesomeIcon | |
aria-label="triangle-down" | |
aria-hidden={false} | |
icon={faTriangle} | |
className="fa-rotate-180" | |
style={{ | |
fontSize: '12px', | |
color: theme.palette.text.primary, | |
}} | |
/> | |
} | |
defaultExpandIcon={ | |
<FontAwesomeIcon | |
aria-label="triangle-right" | |
aria-hidden={false} | |
icon={faTriangleRegular} | |
className="fa-rotate-90" | |
style={{ | |
fontSize: '12px', | |
color: theme.palette.text.primary, | |
}} | |
/> | |
} | |
onNodeSelect={handleOnNodeSelect} | |
onNodeToggle={(_, nodeIds) => setExpanded(nodeIds)} | |
> | |
{transformedItems.map(renderTreeOverride ?? renderTree)} | |
</MuiTreeView> | |
)} | |
</BodyGrid> | |
</Grid> | |
); | |
} | |
export default TreeView; | |
const BodyGrid = styled(Grid)(({ theme }) => ({ | |
flex: 1, | |
border: '1px solid ' + theme.palette.protechtGrey?.grey_231, | |
borderRadius: '4px', | |
overflow: 'auto', | |
})); | |
const LabelIconWrapper = styled(Box)({ | |
marginRight: '6px', | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type TreeItem = { | |
id: string | number; | |
label: string; | |
isHighlighted?: boolean; | |
}; | |
export type TreeNode<T extends TreeItem> = { | |
item: T; | |
children?: TreeNode<T>[]; | |
expanded?: boolean; | |
}; | |
export type Tree<T extends TreeItem> = TreeNode<T>[]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment