1- import { useAtom , useAtomValue } from 'jotai' ;
2- import { useCallback } from 'react' ;
3- import { VscChevronDown , VscChevronRight , VscFile } from 'react-icons/vsc' ;
1+ import { useAtom , useAtomValue , useSetAtom } from 'jotai' ;
2+ import { useCallback , useMemo } from 'react' ;
43
5- import { fileExplorerEntryPathBeingRenamed } from '../../atoms/fileExplorerActions' ;
6- import { FileExplorerFolderEntry } from '../../atoms/types/FileExplorerEntry' ;
7- import { FileNode } from '../../components/FileNode' ;
4+ import {
5+ fileExplorerAtom ,
6+ fileExplorerEntryPathBeingRenamed ,
7+ refreshFileTreeAtom ,
8+ } from '../../atoms/fileExplorerActions' ;
9+ import { useActiveEditorIdForPane } from '../../atoms/hooks/useActiveEditorIdForPane' ;
10+ import { parseEditorId } from '../../atoms/types/EditorData' ;
11+ import { FileExplorerFileEntry , FileExplorerFolderEntry } from '../../atoms/types/FileExplorerEntry' ;
12+ import { FileNameInput } from '../../components/FileNameInput' ;
813import { emitEvent } from '../../events' ;
14+ import { useAsyncEffect } from '../../hooks/useAsyncEffect' ;
15+ import { cx } from '../../lib/cx' ;
16+ import { isNonNullish } from '../../lib/isNonNullish' ;
917import { FILE_EXPLORER_FILE_MENU_ID } from './fileExplorerContextMenu/FileExplorerFileContextMenu' ;
18+ import { useFileExplorerContextMenu } from './fileExplorerContextMenu/useFileExplorerContextMenu' ;
19+ import { ArrowDownIcon , ArrowRightIcon } from './icons' ;
1020
11- interface FileExplorerProps {
12- fileExplorerEntry : FileExplorerFolderEntry ;
13- onFileClick : ( filePath : string ) => void ;
14- selectedFiles : string [ ] ;
15- paddingLeft ?: string ;
21+ export function FileExplorer ( ) {
22+ const rootFileExplorerEntry = useAtomValue ( fileExplorerAtom ) ;
23+ const projectFileEntries = useAtomValue ( rootFileExplorerEntry . childrenAtom ) ;
24+
25+ const refreshFileTree = useSetAtom ( refreshFileTreeAtom ) ;
26+
27+ useAsyncEffect ( refreshFileTree ) ;
28+
29+ return (
30+ < >
31+ { projectFileEntries . map ( ( fileEntry ) =>
32+ fileEntry . isFolder ? (
33+ < FolderNode folder = { fileEntry } key = { fileEntry . path } />
34+ ) : (
35+ < FileNode
36+ existingFileNames = { projectFileEntries . map ( ( file ) => file . name ) }
37+ file = { fileEntry }
38+ key = { fileEntry . path }
39+ />
40+ ) ,
41+ ) }
42+ </ >
43+ ) ;
1644}
1745
18- export function FileExplorer ( props : FileExplorerProps ) {
19- const { fileExplorerEntry, onFileClick, selectedFiles, paddingLeft = '0' } = props ;
20- const files = useAtomValue ( fileExplorerEntry . childrenAtom ) ;
46+ interface FolderNodeProps {
47+ folder : FileExplorerFolderEntry ;
48+ }
49+ export function FolderNode ( { folder } : FolderNodeProps ) {
50+ const childrenEntries = useAtomValue ( folder . childrenAtom ) ;
51+ const [ collapsed , setCollapsed ] = useAtom ( folder . collapsedAtom ) ;
52+
53+ return (
54+ < div className = "flex flex-col items-start gap-2 self-stretch" >
55+ < div
56+ className = { cx ( 'flex cursor-pointer select-none items-start gap-1 self-stretch' ) }
57+ onClick = { ( ) => setCollapsed ( ( currentState ) => ! currentState ) }
58+ >
59+ < div className = "text-btn-ico-side-bar-item" > { collapsed ? < ArrowRightIcon /> : < ArrowDownIcon /> } </ div >
60+ < h2 className = "overflow-hidden overflow-ellipsis whitespace-nowrap" > { folder . name } </ h2 >
61+ </ div >
62+ { ! collapsed && (
63+ < div className = "flex flex-col items-start gap-2 self-stretch pl-5" >
64+ { childrenEntries . map ( ( child ) =>
65+ child . isFolder ? (
66+ < FolderNode folder = { child } key = { child . path } />
67+ ) : (
68+ < FileNode existingFileNames = { childrenEntries . map ( ( file ) => file . name ) } file = { child } key = { child . path } />
69+ ) ,
70+ ) }
71+ </ div >
72+ ) }
73+ </ div >
74+ ) ;
75+ }
2176
77+ interface FileNodeProps {
78+ file : FileExplorerFileEntry ;
79+ existingFileNames : string [ ] ;
80+ }
81+ export function FileNode ( { file, existingFileNames } : FileNodeProps ) {
2282 const [ pathBeingRenamed , setPathBeingRenamed ] = useAtom ( fileExplorerEntryPathBeingRenamed ) ;
2383
24- const [ collapsed , setCollapsed ] = useAtom ( fileExplorerEntry . collapsedAtom ) ;
84+ const leftPaneActiveEditorId = useActiveEditorIdForPane ( 'LEFT' ) ;
85+ const rightPaneActiveEditorId = useActiveEditorIdForPane ( 'RIGHT' ) ;
86+ const isFileActive = useMemo (
87+ ( ) =>
88+ [ leftPaneActiveEditorId , rightPaneActiveEditorId ]
89+ . filter ( isNonNullish )
90+ . map ( ( editorId ) => parseEditorId ( editorId ) . id )
91+ . includes ( file . path ) ,
92+ [ leftPaneActiveEditorId , rightPaneActiveEditorId , file . path ] ,
93+ ) ;
94+
95+ const show = useFileExplorerContextMenu ( FILE_EXPLORER_FILE_MENU_ID , { id : file . path } ) ;
2596
2697 const isNameValid = useCallback (
2798 ( name : string ) => {
@@ -32,55 +103,34 @@ export function FileExplorer(props: FileExplorerProps) {
32103 return false ;
33104 }
34105
35- return ! files . find ( ( file ) => file . name === name ) ;
106+ return ! existingFileNames . includes ( name ) ;
36107 } ,
37- [ files ] ,
108+ [ existingFileNames ] ,
38109 ) ;
39110
40- const { root } = fileExplorerEntry ;
41- return (
42- < div className = "flex w-full flex-col" >
43- { ! root && (
44- < FileNode
45- VscIcon = { collapsed ? VscChevronRight : VscChevronDown }
46- bold
47- fileId = { fileExplorerEntry . path }
48- fileName = { fileExplorerEntry . name }
49- paddingLeft = { `calc(${ paddingLeft } - 1rem)` }
50- onClick = { ( ) => setCollapsed ( ! collapsed ) }
51- />
52- ) }
53- { ( root || ! collapsed ) && (
54- < >
55- { files . map ( ( fileEntry ) =>
56- fileEntry . isFolder ? (
57- < FileExplorer
58- { ...props }
59- fileExplorerEntry = { fileEntry }
60- key = { fileEntry . path }
61- paddingLeft = { `calc(${ paddingLeft } + 1rem)` }
62- />
63- ) : (
64- < FileNode
65- VscIcon = { VscFile }
66- contextMenuId = { FILE_EXPLORER_FILE_MENU_ID }
67- fileId = { fileEntry . path }
68- fileName = { fileEntry . name }
69- isEditMode = { pathBeingRenamed === fileEntry . path }
70- isNameValid = { isNameValid }
71- key = { fileEntry . path }
72- paddingLeft = { paddingLeft }
73- selected = { selectedFiles . includes ( fileEntry . path ) }
74- onCancelRename = { ( ) => setPathBeingRenamed ( null ) }
75- onClick = { ( ) => onFileClick ( fileEntry . path ) }
76- onFileRename = { ( newName : string ) =>
77- emitEvent ( 'refstudio://explorer/rename' , { path : fileEntry . path , newName } )
78- }
79- />
80- ) ,
81- ) }
82- </ >
111+ return pathBeingRenamed === file . path ? (
112+ < FileNameInput
113+ fileName = { file . name }
114+ isNameValid = { isNameValid }
115+ onCancel = { ( ) => setPathBeingRenamed ( null ) }
116+ onSubmit = { ( newName : string ) => emitEvent ( 'refstudio://explorer/rename' , { path : file . path , newName } ) }
117+ />
118+ ) : (
119+ < div
120+ className = { cx (
121+ 'flex shrink-0 items-center gap-1 self-stretch px-2 py-1' ,
122+ 'text-btn-txt-side-bar-item-primary' ,
123+ 'rounded-default hover:bg-btn-bg-side-bar-item-hover' ,
124+ 'cursor-pointer select-none' ,
125+ {
126+ 'bg-btn-bg-side-bar-item-default' : ! isFileActive ,
127+ 'bg-btn-bg-side-bar-item-active' : isFileActive ,
128+ } ,
83129 ) }
130+ onClick = { ( ) => emitEvent ( 'refstudio://explorer/open' , { path : file . path } ) }
131+ onContextMenu = { show }
132+ >
133+ < div className = "overflow-hidden overflow-ellipsis whitespace-nowrap" > { file . name } </ div >
84134 </ div >
85135 ) ;
86136}
0 commit comments