Skip to content

Commit a322cd0

Browse files
authored
feat: explorer panel styles (#435)
* feat: update styles for file explorer * feat: update tests * feat: add ellipsis for long file names * fix: run prettier * fix: remove unused export * fix: remove duplicated logic
1 parent ec83457 commit a322cd0

13 files changed

Lines changed: 262 additions & 425 deletions

File tree

src/application/sidebar/ExplorerPanel.tsx

Lines changed: 18 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
1-
import { useAtomValue, useSetAtom } from 'jotai';
2-
import { VscClose, VscEmptyWindow } from 'react-icons/vsc';
1+
import { useAtomValue } from 'jotai';
32

4-
import { openFilePathAtom } from '../../atoms/fileEntryActions';
5-
import { fileExplorerAtom, refreshFileTreeAtom } from '../../atoms/fileExplorerActions';
6-
import { useActiveEditorIdForPane } from '../../atoms/hooks/useActiveEditorIdForPane';
7-
import { isProjectOpenAtom, projectNameAtom } from '../../atoms/projectState';
8-
import { parseEditorId } from '../../atoms/types/EditorData';
9-
import { PanelSection } from '../../components/PanelSection';
3+
import { isProjectOpenAtom } from '../../atoms/projectState';
4+
import { CloseIcon, OpenIcon } from '../../components/icons';
105
import { PanelWrapper } from '../../components/PanelWrapper';
116
import { emitEvent } from '../../events';
12-
import { useAsyncEffect } from '../../hooks/useAsyncEffect';
13-
import { isNonNullish } from '../../lib/isNonNullish';
147
import { FileExplorer } from './FileExplorer';
158

169
export function ExplorerPanel() {
1710
const isProjectOpen = useAtomValue(isProjectOpenAtom);
1811

12+
const openProject = () => emitEvent('refstudio://menu/file/project/open');
13+
const closeProject = () => emitEvent('refstudio://menu/file/project/close');
14+
1915
return (
20-
<PanelWrapper title="Explorer">
21-
{isProjectOpen && <ProjectSection />}
16+
<PanelWrapper
17+
actions={[
18+
{ key: 'open', Icon: <OpenIcon />, title: 'Open Project', onClick: openProject },
19+
{ key: 'close', disabled: !isProjectOpen, Icon: <CloseIcon />, title: 'Close Project', onClick: closeProject },
20+
]}
21+
title="Project"
22+
>
23+
{isProjectOpen && (
24+
<div className="flex flex-1 flex-col items-start gap-2 self-stretch overflow-y-auto overflow-x-hidden p-4 pt-2">
25+
<FileExplorer />
26+
</div>
27+
)}
2228

2329
{!isProjectOpen && (
2430
<div className="p-4">
@@ -33,40 +39,6 @@ export function ExplorerPanel() {
3339
);
3440
}
3541

36-
export function ProjectSection() {
37-
const leftPaneActiveEditorId = useActiveEditorIdForPane('LEFT');
38-
const rightPaneActiveEditorId = useActiveEditorIdForPane('RIGHT');
39-
const rootFileExplorerEntry = useAtomValue(fileExplorerAtom);
40-
const openFile = useSetAtom(openFilePathAtom);
41-
const refreshFileTree = useSetAtom(refreshFileTreeAtom);
42-
const projectName = useAtomValue(projectNameAtom);
43-
44-
useAsyncEffect(refreshFileTree);
45-
46-
const openProject = () => emitEvent('refstudio://menu/file/project/open');
47-
const closeProject = () => emitEvent('refstudio://menu/file/project/close');
48-
49-
return (
50-
<PanelSection
51-
grow
52-
rightIcons={[
53-
{ key: 'open', Icon: VscEmptyWindow, title: 'Open Project', onClick: openProject },
54-
{ key: 'close', Icon: VscClose, title: 'Close Project', onClick: closeProject },
55-
]}
56-
title={projectName}
57-
>
58-
<FileExplorer
59-
fileExplorerEntry={rootFileExplorerEntry}
60-
paddingLeft="1.25rem"
61-
selectedFiles={[leftPaneActiveEditorId, rightPaneActiveEditorId]
62-
.filter(isNonNullish)
63-
.map((editorId) => parseEditorId(editorId).id)}
64-
onFileClick={openFile}
65-
/>
66-
</PanelSection>
67-
);
68-
}
69-
7042
function CreateNewProjectAction() {
7143
const handleClick = () => emitEvent('refstudio://menu/file/project/new');
7244
return <ProjectAction onClick={handleClick}>Create New Project</ProjectAction>;
Lines changed: 110 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,98 @@
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';
813
import { emitEvent } from '../../events';
14+
import { useAsyncEffect } from '../../hooks/useAsyncEffect';
15+
import { cx } from '../../lib/cx';
16+
import { isNonNullish } from '../../lib/isNonNullish';
917
import { 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

Comments
 (0)