diff --git a/src/TreeView/TreeView.luau b/src/TreeView/TreeView.luau index 2a9c4eec..1b3dffd5 100644 --- a/src/TreeView/TreeView.luau +++ b/src/TreeView/TreeView.luau @@ -8,7 +8,9 @@ type PartialTreeNode = types.PartialTreeNode type TreeNode = types.TreeNode type Tree = types.Tree -local function TreeView() +local function TreeView(props: { + LayoutOrder: number?, +}) local treeViewContext = TreeViewContext.use() local children: { [string]: React.Node } = {} @@ -22,6 +24,7 @@ local function TreeView() return React.createElement("Frame", { BackgroundTransparency = 1, AutomaticSize = Enum.AutomaticSize.XY, + LayoutOrder = props.LayoutOrder, }, { Layout = React.createElement("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, diff --git a/src/TreeView/TreeView.story.luau b/src/TreeView/TreeView.story.luau index 87e91f0f..d9aa3f4d 100644 --- a/src/TreeView/TreeView.story.luau +++ b/src/TreeView/TreeView.story.luau @@ -2,11 +2,13 @@ local React = require("@pkg/React") local ContextProviders = require("@root/Common/ContextProviders") local MockPlugin = require("@root/Testing/MockPlugin") +local Searchbar = require("@root/Forms/Searchbar") local TreeView = require("./TreeView") local TreeViewContext = require("@root/TreeView/TreeViewContext") local types = require("./types") local useEffect = React.useEffect +local useState = React.useState type PartialTreeNode = types.PartialTreeNode type TreeNode = types.TreeNode @@ -104,14 +106,29 @@ local function Story() treeViewContext.setRoots(roots) end, {}) + local searchTerm, setSearchTerm = useState(nil :: string?) + + useEffect(function() + treeViewContext.search(searchTerm) + end, { treeViewContext, searchTerm } :: { unknown }) + return React.createElement("Frame", { Size = UDim2.fromOffset(300, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, }, { + Layout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 16), + }), + + Search = React.createElement(Searchbar, { + onSearchChanged = setSearchTerm, + LayoutOrder = 1, + }), TreeView = React.createElement(TreeView, { - roots = roots, + LayoutOrder = 2, }), }) end diff --git a/src/TreeView/TreeViewContext.luau b/src/TreeView/TreeViewContext.luau index 79efac08..ff961249 100644 --- a/src/TreeView/TreeViewContext.luau +++ b/src/TreeView/TreeViewContext.luau @@ -2,6 +2,8 @@ local React = require("@pkg/React") local Sift = require("@pkg/Sift") local createTreeNodesFromPartials = require("@root/TreeView/createTreeNodesFromPartials") +local getAncestry = require("@root/TreeView/getAncestry") +local reduceTree = require("@root/TreeView/reduceTree") local types = require("@root/TreeView/types") type PartialTreeNode = types.PartialTreeNode @@ -9,24 +11,16 @@ type TreeNode = types.TreeNode local useCallback = React.useCallback local useContext = React.useContext +local useMemo = React.useMemo local useState = React.useState -local function getAncestry(node: TreeNode): { TreeNode } - local ancestry = {} - local parent = node.parent - while parent do - table.insert(ancestry, parent) - parent = parent.parent - end - return ancestry -end - type TreeViewContext = { setRoots: (nodes: { PartialTreeNode }) -> (), getRoots: () -> { TreeNode }, activateNode: (node: TreeNode) -> (), isExpanded: (node: TreeNode) -> boolean, isSelected: (node: TreeNode) -> boolean, + search: (searchTerm: string) -> (), } local TreeViewContext = React.createContext(nil) @@ -41,6 +35,10 @@ local function TreeNodeProvider(props: { local expandedNodes, setExpandedNodes = useState({} :: { TreeNode }) local selectedNode, setSelectedNode = useState(nil :: TreeNode?) + local searchTerm: string?, setSearchTerm = useState(nil :: string?) + + -- TODO: Expand to a specific node + -- TODO: Always put folders at the top of the list local expand = useCallback(function(node: TreeNode) setExpandedNodes(function(prev) @@ -67,6 +65,24 @@ local function TreeNodeProvider(props: { end) end, {}) + local filteredRoots = useMemo(function() + if searchTerm then + return reduceTree(nodes.roots, function(node) + return node.label:lower():match(searchTerm:lower()) ~= nil + end) + else + return nodes.roots + end + end, { nodes.roots, searchTerm } :: { unknown }) + + local search = useCallback(function(newSearchTerm: string) + if newSearchTerm ~= "" then + setSearchTerm(newSearchTerm) + else + setSearchTerm(nil) + end + end, {}) + local setRoots = useCallback(function(partials: { PartialTreeNode }) local newNodes = createTreeNodesFromPartials(partials) @@ -81,16 +97,16 @@ local function TreeNodeProvider(props: { end, {}) local getRoots = useCallback(function() - return nodes.roots - end, { nodes }) - - -- local getLeaves = useCallback(function() - -- return nodes.leaves - -- end, { nodes }) + return filteredRoots + end, { filteredRoots }) local isExpanded = useCallback(function(node: TreeNode): boolean - return table.find(expandedNodes, node) ~= nil - end, { expandedNodes }) + if searchTerm then + return true + else + return table.find(expandedNodes, node) ~= nil + end + end, { expandedNodes, searchTerm } :: { unknown }) local isSelected = useCallback(function(node: TreeNode): boolean return selectedNode == node @@ -118,6 +134,7 @@ local function TreeNodeProvider(props: { activateNode = activateNode, isExpanded = isExpanded, isSelected = isSelected, + search = search, } return React.createElement(TreeViewContext.Provider, { diff --git a/src/TreeView/getAncestry.luau b/src/TreeView/getAncestry.luau new file mode 100644 index 00000000..ebfd4656 --- /dev/null +++ b/src/TreeView/getAncestry.luau @@ -0,0 +1,15 @@ +local types = require("./types") + +type TreeNode = types.TreeNode + +local function getAncestry(node: TreeNode): { TreeNode } + local ancestry = {} + local parent = node.parent + while parent do + table.insert(ancestry, parent) + parent = parent.parent + end + return ancestry +end + +return getAncestry diff --git a/src/TreeView/reduceTree.luau b/src/TreeView/reduceTree.luau new file mode 100644 index 00000000..de70f2ea --- /dev/null +++ b/src/TreeView/reduceTree.luau @@ -0,0 +1,34 @@ +local Sift = require("@pkg/Sift") + +local types = require("@root/TreeView/types") + +type TreeNode = types.TreeNode +type SearchMatch = (node: TreeNode) -> boolean + +local function reduceTree(nodes: { TreeNode }, searchMatch: SearchMatch): { TreeNode } + local function reduceNodes(accumulator: { TreeNode }, node: TreeNode) + if searchMatch(node) then + table.insert(accumulator, node) + return accumulator + end + + if #node.children > 0 then + local children = Sift.List.reduce(node.children, reduceNodes, {}) + + if #children > 0 then + table.insert( + accumulator, + Sift.Dictionary.join(node, { + children = children, + }) + ) + end + end + + return accumulator + end + + return Sift.List.reduce(nodes, reduceNodes, {}) +end + +return reduceTree