diff --git a/src/workspaces/officerdev/src/apps/CodeEditor/FileTree.tsx b/src/workspaces/officerdev/src/apps/CodeEditor/FileTree.tsx index 31b1d57c..d76b9ce6 100644 --- a/src/workspaces/officerdev/src/apps/CodeEditor/FileTree.tsx +++ b/src/workspaces/officerdev/src/apps/CodeEditor/FileTree.tsx @@ -21,6 +21,14 @@ type FileTreeProps = { * an agent had just written to showing the listing from before its turn. */ refreshToken?: number; + /** + * A file to reveal: every directory on the way to it opens, loading as it goes. + * + * For a tree whose selection is driven from OUTSIDE — a nav beside it, a URL — where a folder left + * shut around the open file hides the one thing being looked at. Expanding is still disclosure and + * still remembered, so anything opened by hand stays open. + */ + revealPath?: string | null; }; type TreeNodeProps = { @@ -31,6 +39,7 @@ type TreeNodeProps = { searchForFile?: ((path: string) => string) | null; depth: number; refreshToken?: number; + revealPath?: string | null; }; const FileIcon = ({ name }: { name: string }) => { @@ -44,7 +53,16 @@ const sortEntries = (entries: DirEntry[]) => { return [...dirs, ...files]; }; -const TreeNode = ({ entry, parentPath, root, onOpenFile, searchForFile, depth, refreshToken }: TreeNodeProps) => { +const TreeNode = ({ + entry, + parentPath, + root, + onOpenFile, + searchForFile, + depth, + refreshToken, + revealPath, +}: TreeNodeProps) => { const [expanded, setExpanded] = useState(false); const [children, setChildren] = useState(null); const [loading, setLoading] = useState(false); @@ -73,6 +91,32 @@ const TreeNode = ({ entry, parentPath, root, onOpenFile, searchForFile, depth, r }; }, [refreshToken]); + /* + On the way to the revealed file: open, and load if this is the first time. + + Only ever opens. A node that closes itself when the selection moves elsewhere would collapse the + tree under somebody as they clicked around it. + */ + const onTheWay = !!revealPath && isDir && revealPath.startsWith(`${fullPath}/`); + + useEffect(() => { + if (!onTheWay) return; + setExpanded(true); + if (children !== null) return; + + let alive = true; + listDirRef + .current(fullPath) + .then((res) => alive && setChildren(sortEntries(res.entries))) + .catch(() => alive && setChildren([])); + return () => { + alive = false; + }; + // `children` is deliberately absent: this must run when the PATH changes, not when the load it + // started completes. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onTheWay, fullPath]); + const handleClick = async () => { if (!isDir) { onOpenFile(fullPath, entry.name); @@ -140,6 +184,7 @@ const TreeNode = ({ entry, parentPath, root, onOpenFile, searchForFile, depth, r searchForFile={searchForFile} depth={depth + 1} refreshToken={refreshToken} + revealPath={revealPath} /> ))} @@ -148,7 +193,7 @@ const TreeNode = ({ entry, parentPath, root, onOpenFile, searchForFile, depth, r ); }; -export const FileTree = ({ root, basePath, onOpenFile, searchForFile, refreshToken }: FileTreeProps) => { +export const FileTree = ({ root, basePath, onOpenFile, searchForFile, refreshToken, revealPath }: FileTreeProps) => { const { t } = useTranslation(); const { listDir } = useFilesAPI(root); const [entries, setEntries] = useState(null); @@ -208,6 +253,7 @@ export const FileTree = ({ root, basePath, onOpenFile, searchForFile, refreshTok searchForFile={searchForFile} depth={0} refreshToken={refreshToken} + revealPath={revealPath} /> ))}