diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index d044bff7..fb28d9a6 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -67,13 +67,7 @@ export const Sidebar = ({ if (!tree.current) return; tree.current.scrollToItem(activeFilePath, "center"); }, 100); - }, []); - useEffect(() => { - if (!tree.current) return; - tree.current.recomputeTree({ - useDefaultOpenness: true, - }); - }, [searchTerm]); + }, [activeFilePath]); const nestedFiles = useMemo(() => { try { @@ -82,18 +76,27 @@ export const Sidebar = ({ }, [files]); const filteredFiles = useMemo(() => { - if (!searchTerm) return nestedFiles; const lowerSearchTerm = searchTerm.toLowerCase(); - return nestedFiles - .map((file) => { - const children = file.children.filter((child) => - child.name.toLowerCase().includes(lowerSearchTerm) - ); - const isMatch = file.name.toLowerCase().includes(lowerSearchTerm); - if (!children.length && !isMatch) return null; - return { ...file, children }; - }) - .filter(Boolean) as NestedFileTree[]; + return [ + { + name: `${owner}/${repo}`, + path: "", + children: !searchTerm + ? nestedFiles + : (nestedFiles + .map((file) => { + const children = file.children.filter((child) => + child.name.toLowerCase().includes(lowerSearchTerm) + ); + const isMatch = file.name + .toLowerCase() + .includes(lowerSearchTerm); + if (!children.length && !isMatch) return null; + return { ...file, children }; + }) + .filter(Boolean) as NestedFileTree[]), + }, + ]; }, [nestedFiles, searchTerm]); const numberOfFiles = useMemo(() => { @@ -106,80 +109,58 @@ export const Sidebar = ({ return runningCount; }, [filteredFiles]); - const treeWalker = useMemo( - () => - function* treeWalker(refresh) { - const stack = []; - - // Remember all the necessary data of the first node in the stack. - stack.push({ - depth: 0, - node: { - id: "/", - name: `${owner}/${repo}`, - path: "", - children: filteredFiles, - }, - }); + // This function prepares an object for yielding. We can yield an object + // that has `data` object with `id` and `isOpenByDefault` fields. + // We can also add any other data here. + const getNodeData = (node, nestingLevel) => { + const id = node.path || "/"; + // don't close open folders + const existingIsOpen = tree.current?.state.records.get(id)?.public.isOpen; + return { + data: { + id, + isLeaf: node.children?.length === 0, + isOpenByDefault: + existingIsOpen || + activeFilePath === id || + activeFilePath.startsWith(`${id}/`) || + numberOfFiles < 20 || + nestingLevel < 1, + name: node.name, + depth: nestingLevel, + path: node.path, + activeFilePath, + canCollapse: nestingLevel > 0, + updatedContents, + currentPathname: router.pathname, + currentQuery: query, + currentBranch: branchName, + }, + nestingLevel, + node, + }; + }; + + function* treeWalker() { + // Step [1]: Define the root node of our tree. There can be one or + // multiple nodes. + for (let i = 0; i < filteredFiles.length; i++) { + yield getNodeData(filteredFiles[i], 0); + } - // Walk through the tree until we have no nodes available. - while (stack.length !== 0) { - const { - node: { children = [], path, name }, - depth, - } = stack.pop(); - const canCollapse = depth > 0; - const id = path || "/"; + while (true) { + // Step [2]: Get the parent component back. It will be the object + // the `getNodeData` function constructed, so you can read any data from it. + const parent = yield; - // Here we are sending the information about the node to the Tree component - // and receive an information about the openness state from it. The - // `refresh` parameter tells us if the full update of the tree is requested; - // basing on it we decide to return the full node data or only the node - // id to update the nodes order. - const isOpened = yield refresh - ? { - id, - isLeaf: children.length === 0, - isOpenByDefault: - activeFilePath === id || - activeFilePath.startsWith(`${id}/`) || - numberOfFiles < 20 || - depth < 1, - name, - depth, - path, - activeFilePath, - canCollapse, - updatedContents, - currentPathname: router.pathname, - currentQuery: query, - currentBranch: branchName, - } - : id; + for (let i = 0; i < parent.node.children.length; i++) { + // Step [3]: Yielding all the children of the provided component. Then we + // will return for the step [2] with the first children. + yield getNodeData(parent.node.children[i], parent.nestingLevel + 1); + } + } + } - // Basing on the node openness state we are deciding if we need to render - // the child nodes (if they exist). - if (children.length !== 0 && isOpened) { - // Since it is a stack structure, we need to put nodes we want to render - // first to the end of the stack. - for (let i = children.length - 1; i >= 0; i--) { - stack.push({ - depth: depth + 1, - node: children[i], - }); - } - } - } - }, - [ - filteredFiles, - updatedContents, - numberOfFiles < 20, - activeFilePath, - query, - branchName, - ] - ); if (!files.map) return null; return ( @@ -202,6 +183,7 @@ export const Sidebar = ({ height={dimensions[1]} width={dimensions[0]} className="pb-10" + ref={tree} > {Node} @@ -218,7 +200,6 @@ export const Sidebar = ({ const Node = ({ data: { - isLeaf, name, path, activeFilePath, @@ -228,10 +209,11 @@ const Node = ({ currentPathname, currentQuery, currentBranch, + isLeaf, }, isOpen, style, - toggle, + setOpen, }) => { return (
@@ -245,7 +227,7 @@ const Node = ({ currentPathname={currentPathname} currentQuery={currentQuery} currentBranch={currentBranch} - toggle={toggle} + setOpen={setOpen} isOpen={isOpen} isFolder={!isLeaf} /> @@ -265,7 +247,7 @@ type ItemProps = { canCollapse?: boolean; isOpen: boolean; updatedContents: Record; - toggle: () => void; + setOpen: (newState: boolean) => void; }; const Item = ({ @@ -279,7 +261,7 @@ const Item = ({ currentBranch, isOpen, updatedContents, - toggle, + setOpen, }: ItemProps) => { const isActive = activeFilePath === path; @@ -312,7 +294,7 @@ const Item = ({ // don't select the folder on toggle e.stopPropagation(); e.preventDefault(); - toggle(); + setOpen(!isOpen); }} > {depth > 0 && ( diff --git a/package.json b/package.json index ef4ea311..b79abb23 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "react-icons": "^4.3.1", "react-merge-refs": "^2.0.1", "react-query": "^3.34.16", - "react-vtree": "^2.0.4", + "react-vtree": "3.0.0-beta.3", "react-window": "^1.8.7", "streamifier": "^0.1.1", "styled-components": "^5.3.3", @@ -78,7 +78,8 @@ "resolutions": { "trim": "0.0.3", "diff": "3.5.0", - "prismjs": "1.27.0" + "prismjs": "1.27.0", + "@octokit/types": "8.0.0" }, "volta": { "node": "16.18.1" diff --git a/yarn.lock b/yarn.lock index c9a6a585..9be0064a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -817,6 +817,11 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== +"@octokit/openapi-types@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-14.0.0.tgz#949c5019028c93f189abbc2fb42f333290f7134a" + integrity sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw== + "@octokit/plugin-paginate-rest@^2.16.8": version "2.17.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7" @@ -868,7 +873,14 @@ "@octokit/plugin-request-log" "^1.0.4" "@octokit/plugin-rest-endpoint-methods" "^5.12.0" -"@octokit/types@^6.0.0", "@octokit/types@^6.0.3", "@octokit/types@^6.10.0", "@octokit/types@^6.12.2", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0": +"@octokit/types@8.0.0", "@octokit/types@^6.0.3", "@octokit/types@^6.10.0", "@octokit/types@^6.12.2", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.0.0.tgz#93f0b865786c4153f0f6924da067fe0bb7426a9f" + integrity sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg== + dependencies: + "@octokit/openapi-types" "^14.0.0" + +"@octokit/types@^6.0.0": version "6.41.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04" integrity sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg== @@ -4021,6 +4033,11 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-merge-refs@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06" + integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ== + react-merge-refs@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-2.0.1.tgz#a1f8c2dadefa635333e9b91ec59f30b65228b006" @@ -4077,12 +4094,13 @@ react-style-singleton@^2.1.0: invariant "^2.2.4" tslib "^1.0.0" -react-vtree@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/react-vtree/-/react-vtree-2.0.4.tgz#340e64255f5f4ec6f4c35dc44a7036f7fcd98bc5" - integrity sha512-UOld0VqyAZrryF06K753X4bcEVN6/wW831exvVlMZeZAVHk9KXnlHs4rpqDAeoiBgUwJqoW/rtn0hwsokRRxPA== +react-vtree@3.0.0-beta.3: + version "3.0.0-beta.3" + resolved "https://registry.yarnpkg.com/react-vtree/-/react-vtree-3.0.0-beta.3.tgz#9a2dfc31fa730c39d19b0dff7a9df81ead816bd5" + integrity sha512-BGC8kOT2Ti3rne0Nwu+n90TAo8lbYiWT36Cu47aj6bz+Bs7k5p3EVgBTinyuCdU5+n4a9wJOXHAdop/zsR1RAA== dependencies: "@babel/runtime" "^7.11.0" + react-merge-refs "^1.1.0" react-window@^1.8.7: version "1.8.7"