| ... | @@ -3,9 +3,9 @@ import type { | ... | @@ -3,9 +3,9 @@ import type { |
| 3 | ContextMenuOpenContext as TreeContextMenuOpenContext, | 3 | ContextMenuOpenContext as TreeContextMenuOpenContext, |
| 4 | } from "@pierre/trees"; | 4 | } from "@pierre/trees"; |
| 5 | import type { EnvironmentId, ProjectEntry } from "@t3tools/contracts"; | 5 | import 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"; |
| 7 | import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger"; | 7 | import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger"; |
| 8 | import { RotateCw } from "lucide-react"; | 8 | import { ChevronsDownUpIcon, ChevronsUpDownIcon, RotateCw } from "lucide-react"; |
| 9 | import { useEffect, useMemo, useRef } from "react"; | 9 | import { useEffect, useMemo, useRef } from "react"; |
| 10 | | 10 | |
| 11 | import { Button } from "~/components/ui/button"; | 11 | import { Button } from "~/components/ui/button"; |
| ... | @@ -21,6 +21,7 @@ import { readLocalApi } from "~/localApi"; | ... | @@ -21,6 +21,7 @@ import { readLocalApi } from "~/localApi"; |
| 21 | import { T3_PIERRE_ICONS } from "~/pierre-icons"; | 21 | import { T3_PIERRE_ICONS } from "~/pierre-icons"; |
| 22 | | 22 | |
| 23 | import { createFileTreeDragMentionController } from "./fileTreeDragMention"; | 23 | import { createFileTreeDragMentionController } from "./fileTreeDragMention"; |
| | 24 | import { areAllDirectoriesExpanded, setAllDirectoriesExpanded } from "./fileTreeExpansion"; |
| 24 | import { useProjectEntriesQuery } from "./projectFilesQueryState"; | 25 | import { useProjectEntriesQuery } from "./projectFilesQueryState"; |
| 25 | | 26 | |
| 26 | interface FileBrowserPanelProps { | 27 | interface FileBrowserPanelProps { |
| ... | @@ -121,6 +122,10 @@ export default function FileBrowserPanel({ | ... | @@ -121,6 +122,10 @@ export default function FileBrowserPanel({ |
| 121 | ); | 122 | ); |
| 122 | const entryKindsRef = useRef<ReadonlyMap<string, ProjectEntry["kind"]>>(entryKinds); | 123 | const entryKindsRef = useRef<ReadonlyMap<string, ProjectEntry["kind"]>>(entryKinds); |
| 123 | const treePaths = useMemo(() => entries.map(treePath), [entries]); | 124 | const treePaths = useMemo(() => entries.map(treePath), [entries]); |
| | 125 | const directoryPaths = useMemo( |
| | 126 | () => entries.filter((entry) => entry.kind === "directory").map(treePath), |
| | 127 | [entries], |
| | 128 | ); |
| 124 | const previousTreePathsRef = useRef<readonly string[]>([]); | 129 | const previousTreePathsRef = useRef<readonly string[]>([]); |
| 125 | const syncingSelectionRef = useRef(false); | 130 | const syncingSelectionRef = useRef(false); |
| 126 | const treeSelectionPathRef = useRef<string | null>(null); | 131 | const treeSelectionPathRef = useRef<string | null>(null); |
| ... | @@ -252,6 +257,12 @@ export default function FileBrowserPanel({ | ... | @@ -252,6 +257,12 @@ export default function FileBrowserPanel({ |
| 252 | unsafeCSS: TREE_UNSAFE_CSS, | 257 | unsafeCSS: TREE_UNSAFE_CSS, |
| 253 | }); | 258 | }); |
| 254 | const search = useFileTreeSearch(model); | 259 | const search = useFileTreeSearch(model); |
| | 260 | const allDirectoriesExpanded = useFileTreeSelector(model, (currentModel) => |
| | 261 | areAllDirectoriesExpanded(currentModel, directoryPaths), |
| | 262 | ); |
| | 263 | const toggleAllDirectories = () => { |
| | 264 | setAllDirectoriesExpanded(model, directoryPaths, !allDirectoriesExpanded); |
| | 265 | }; |
| 255 | const handleSearchValueChange = (value: string) => { | 266 | const handleSearchValueChange = (value: string) => { |
| 256 | if (value.trim().length === 0) { | 267 | if (value.trim().length === 0) { |
| 257 | search.close(); | 268 | search.close(); |
| ... | @@ -376,6 +387,32 @@ export default function FileBrowserPanel({ | ... | @@ -376,6 +387,32 @@ export default function FileBrowserPanel({ |
| 376 | onValueChange={handleSearchValueChange} | 387 | onValueChange={handleSearchValueChange} |
| 377 | onClose={search.close} | 388 | onClose={search.close} |
| 378 | /> | 389 | /> |
| | 390 | {directoryPaths.length > 0 ? ( |
| | 391 | <Tooltip> |
| | 392 | <TooltipTrigger |
| | 393 | render={ |
| | 394 | <Button |
| | 395 | type="button" |
| | 396 | size="icon-xs" |
| | 397 | variant="ghost" |
| | 398 | aria-label={ |
| | 399 | allDirectoriesExpanded ? "Collapse all folders" : "Expand all folders" |
| | 400 | } |
| | 401 | onClick={toggleAllDirectories} |
| | 402 | /> |
| | 403 | } |
| | 404 | > |
| | 405 | {allDirectoriesExpanded ? ( |
| | 406 | <ChevronsDownUpIcon className="size-3.5" /> |
| | 407 | ) : ( |
| | 408 | <ChevronsUpDownIcon className="size-3.5" /> |
| | 409 | )} |
| | 410 | </TooltipTrigger> |
| | 411 | <TooltipPopup> |
| | 412 | {allDirectoriesExpanded ? "Collapse all folders" : "Expand all folders"} |
| | 413 | </TooltipPopup> |
| | 414 | </Tooltip> |
| | 415 | ) : null} |
| 379 | </div> | 416 | </div> |
| 380 | {entriesQuery.error && entriesQuery.data === null ? ( | 417 | {entriesQuery.error && entriesQuery.data === null ? ( |
| 381 | <div className="p-4 text-xs leading-relaxed text-destructive">{entriesQuery.error}</div> | 418 | <div className="p-4 text-xs leading-relaxed text-destructive">{entriesQuery.error}</div> |