@@ -3,9 +3,9 @@ import type {
33 ContextMenuOpenContext as TreeContextMenuOpenContext ,
44} from "@pierre/trees" ;
55import type { EnvironmentId , ProjectEntry } from "@t3tools/contracts" ;
6- import { FileTree , useFileTree , useFileTreeSearch } from "@pierre/trees/react" ;
6+ import { FileTree , useFileTree , useFileTreeSearch , useFileTreeSelector } from "@pierre/trees/react" ;
77import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger" ;
8- import { RotateCw } from "lucide-react" ;
8+ import { ChevronsDownUpIcon , ChevronsUpDownIcon , RotateCw } from "lucide-react" ;
99import { useEffect , useMemo , useRef } from "react" ;
1010
1111import { Button } from "~/components/ui/button" ;
@@ -20,6 +20,7 @@ import { readLocalApi } from "~/localApi";
2020import { T3_PIERRE_ICONS } from "~/pierre-icons" ;
2121
2222import { createFileTreeDragMentionController } from "./fileTreeDragMention" ;
23+ import { areAllDirectoriesExpanded , setAllDirectoriesExpanded } from "./fileTreeExpansion" ;
2324import { useProjectEntriesQuery } from "./projectFilesQueryState" ;
2425
2526interface FileBrowserPanelProps {
@@ -118,6 +119,10 @@ export default function FileBrowserPanel({
118119 ) ;
119120 const entryKindsRef = useRef < ReadonlyMap < string , ProjectEntry [ "kind" ] > > ( entryKinds ) ;
120121 const treePaths = useMemo ( ( ) => entries . map ( treePath ) , [ entries ] ) ;
122+ const directoryPaths = useMemo (
123+ ( ) => entries . filter ( ( entry ) => entry . kind === "directory" ) . map ( treePath ) ,
124+ [ entries ] ,
125+ ) ;
121126 const previousTreePathsRef = useRef < readonly string [ ] > ( [ ] ) ;
122127 const syncingSelectionRef = useRef ( false ) ;
123128 const treeSelectionPathRef = useRef < string | null > ( null ) ;
@@ -249,6 +254,12 @@ export default function FileBrowserPanel({
249254 unsafeCSS : TREE_UNSAFE_CSS ,
250255 } ) ;
251256 const search = useFileTreeSearch ( model ) ;
257+ const allDirectoriesExpanded = useFileTreeSelector ( model , ( currentModel ) =>
258+ areAllDirectoriesExpanded ( currentModel , directoryPaths ) ,
259+ ) ;
260+ const toggleAllDirectories = ( ) => {
261+ setAllDirectoriesExpanded ( model , directoryPaths , ! allDirectoriesExpanded ) ;
262+ } ;
252263 const handleSearchValueChange = ( value : string ) => {
253264 if ( value . trim ( ) . length === 0 ) {
254265 search . close ( ) ;
@@ -367,6 +378,32 @@ export default function FileBrowserPanel({
367378 onValueChange = { handleSearchValueChange }
368379 onClose = { search . close }
369380 />
381+ { directoryPaths . length > 0 ? (
382+ < Tooltip >
383+ < TooltipTrigger
384+ render = {
385+ < Button
386+ type = "button"
387+ size = "icon-xs"
388+ variant = "ghost"
389+ aria-label = {
390+ allDirectoriesExpanded ? "Collapse all folders" : "Expand all folders"
391+ }
392+ onClick = { toggleAllDirectories }
393+ />
394+ }
395+ >
396+ { allDirectoriesExpanded ? (
397+ < ChevronsDownUpIcon className = "size-3.5" />
398+ ) : (
399+ < ChevronsUpDownIcon className = "size-3.5" />
400+ ) }
401+ </ TooltipTrigger >
402+ < TooltipPopup >
403+ { allDirectoriesExpanded ? "Collapse all folders" : "Expand all folders" }
404+ </ TooltipPopup >
405+ </ Tooltip >
406+ ) : null }
370407 </ div >
371408 { entriesQuery . error && entriesQuery . data === null ? (
372409 < div className = "p-4 text-xs leading-relaxed text-destructive" > { entriesQuery . error } </ div >
0 commit comments