Skip to content

Commit 25fe152

Browse files
authored
Merge pull request #602 from mark-when/contextMenu2
Feat: Basic file tree context menu
2 parents b8e457e + 6eb2d84 commit 25fe152

File tree

3 files changed

+146
-33
lines changed

3 files changed

+146
-33
lines changed

app/components/workbench/FileTree.tsx

Lines changed: 115 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';
22
import type { FileMap } from '~/lib/stores/files';
33
import { classNames } from '~/utils/classNames';
44
import { createScopedLogger, renderLogger } from '~/utils/logger';
5+
import * as ContextMenu from '@radix-ui/react-context-menu';
56

67
const logger = createScopedLogger('FileTree');
78

@@ -110,6 +111,22 @@ export const FileTree = memo(
110111
});
111112
};
112113

114+
const onCopyPath = (fileOrFolder: FileNode | FolderNode) => {
115+
try {
116+
navigator.clipboard.writeText(fileOrFolder.fullPath);
117+
} catch (error) {
118+
logger.error(error);
119+
}
120+
};
121+
122+
const onCopyRelativePath = (fileOrFolder: FileNode | FolderNode) => {
123+
try {
124+
navigator.clipboard.writeText(fileOrFolder.fullPath.substring((rootFolder || '').length));
125+
} catch (error) {
126+
logger.error(error);
127+
}
128+
};
129+
113130
return (
114131
<div className={classNames('text-sm', className, 'overflow-y-auto')}>
115132
{filteredFileList.map((fileOrFolder) => {
@@ -121,6 +138,12 @@ export const FileTree = memo(
121138
selected={selectedFile === fileOrFolder.fullPath}
122139
file={fileOrFolder}
123140
unsavedChanges={unsavedFiles?.has(fileOrFolder.fullPath)}
141+
onCopyPath={() => {
142+
onCopyPath(fileOrFolder);
143+
}}
144+
onCopyRelativePath={() => {
145+
onCopyRelativePath(fileOrFolder);
146+
}}
124147
onClick={() => {
125148
onFileSelect?.(fileOrFolder.fullPath);
126149
}}
@@ -134,6 +157,12 @@ export const FileTree = memo(
134157
folder={fileOrFolder}
135158
selected={allowFolderSelection && selectedFile === fileOrFolder.fullPath}
136159
collapsed={collapsedFolders.has(fileOrFolder.fullPath)}
160+
onCopyPath={() => {
161+
onCopyPath(fileOrFolder);
162+
}}
163+
onCopyRelativePath={() => {
164+
onCopyRelativePath(fileOrFolder);
165+
}}
137166
onClick={() => {
138167
toggleCollapseState(fileOrFolder.fullPath);
139168
}}
@@ -156,58 +185,111 @@ interface FolderProps {
156185
folder: FolderNode;
157186
collapsed: boolean;
158187
selected?: boolean;
188+
onCopyPath: () => void;
189+
onCopyRelativePath: () => void;
159190
onClick: () => void;
160191
}
161192

162-
function Folder({ folder: { depth, name }, collapsed, selected = false, onClick }: FolderProps) {
193+
interface FolderContextMenuProps {
194+
onCopyPath?: () => void;
195+
onCopyRelativePath?: () => void;
196+
children: ReactNode;
197+
}
198+
199+
function ContextMenuItem({ onSelect, children }: { onSelect?: () => void; children: ReactNode }) {
163200
return (
164-
<NodeButton
165-
className={classNames('group', {
166-
'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':
167-
!selected,
168-
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
169-
})}
170-
depth={depth}
171-
iconClasses={classNames({
172-
'i-ph:caret-right scale-98': collapsed,
173-
'i-ph:caret-down scale-98': !collapsed,
174-
})}
175-
onClick={onClick}
201+
<ContextMenu.Item
202+
onSelect={onSelect}
203+
className="flex items-center gap-2 px-2 py-1.5 outline-0 text-sm text-bolt-elements-textPrimary cursor-pointer ws-nowrap text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive rounded-md"
176204
>
177-
{name}
178-
</NodeButton>
205+
<span className="size-4 shrink-0"></span>
206+
<span>{children}</span>
207+
</ContextMenu.Item>
208+
);
209+
}
210+
211+
function FileContextMenu({ onCopyPath, onCopyRelativePath, children }: FolderContextMenuProps) {
212+
return (
213+
<ContextMenu.Root>
214+
<ContextMenu.Trigger>{children}</ContextMenu.Trigger>
215+
<ContextMenu.Portal>
216+
<ContextMenu.Content
217+
style={{ zIndex: 998 }}
218+
className="border border-bolt-elements-borderColor rounded-md z-context-menu bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2 data-[state=open]:animate-in animate-duration-100 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-98 w-56"
219+
>
220+
<ContextMenu.Group className="p-1 border-b-px border-solid border-bolt-elements-borderColor">
221+
<ContextMenuItem onSelect={onCopyPath}>Copy path</ContextMenuItem>
222+
<ContextMenuItem onSelect={onCopyRelativePath}>Copy relative path</ContextMenuItem>
223+
</ContextMenu.Group>
224+
</ContextMenu.Content>
225+
</ContextMenu.Portal>
226+
</ContextMenu.Root>
227+
);
228+
}
229+
230+
function Folder({ folder, collapsed, selected = false, onCopyPath, onCopyRelativePath, onClick }: FolderProps) {
231+
return (
232+
<FileContextMenu onCopyPath={onCopyPath} onCopyRelativePath={onCopyRelativePath}>
233+
<NodeButton
234+
className={classNames('group', {
235+
'bg-transparent text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive hover:bg-bolt-elements-item-backgroundActive':
236+
!selected,
237+
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
238+
})}
239+
depth={folder.depth}
240+
iconClasses={classNames({
241+
'i-ph:caret-right scale-98': collapsed,
242+
'i-ph:caret-down scale-98': !collapsed,
243+
})}
244+
onClick={onClick}
245+
>
246+
{folder.name}
247+
</NodeButton>
248+
</FileContextMenu>
179249
);
180250
}
181251

182252
interface FileProps {
183253
file: FileNode;
184254
selected: boolean;
185255
unsavedChanges?: boolean;
256+
onCopyPath: () => void;
257+
onCopyRelativePath: () => void;
186258
onClick: () => void;
187259
}
188260

189-
function File({ file: { depth, name }, onClick, selected, unsavedChanges = false }: FileProps) {
261+
function File({
262+
file: { depth, name },
263+
onClick,
264+
onCopyPath,
265+
onCopyRelativePath,
266+
selected,
267+
unsavedChanges = false,
268+
}: FileProps) {
190269
return (
191-
<NodeButton
192-
className={classNames('group', {
193-
'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault': !selected,
194-
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
195-
})}
196-
depth={depth}
197-
iconClasses={classNames('i-ph:file-duotone scale-98', {
198-
'group-hover:text-bolt-elements-item-contentActive': !selected,
199-
})}
200-
onClick={onClick}
201-
>
202-
<div
203-
className={classNames('flex items-center', {
270+
<FileContextMenu onCopyPath={onCopyPath} onCopyRelativePath={onCopyRelativePath}>
271+
<NodeButton
272+
className={classNames('group', {
273+
'bg-transparent hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-item-contentDefault':
274+
!selected,
275+
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': selected,
276+
})}
277+
depth={depth}
278+
iconClasses={classNames('i-ph:file-duotone scale-98', {
204279
'group-hover:text-bolt-elements-item-contentActive': !selected,
205280
})}
281+
onClick={onClick}
206282
>
207-
<div className="flex-1 truncate pr-2">{name}</div>
208-
{unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
209-
</div>
210-
</NodeButton>
283+
<div
284+
className={classNames('flex items-center', {
285+
'group-hover:text-bolt-elements-item-contentActive': !selected,
286+
})}
287+
>
288+
<div className="flex-1 truncate pr-2">{name}</div>
289+
{unsavedChanges && <span className="i-ph:circle-fill scale-68 shrink-0 text-orange-500" />}
290+
</div>
291+
</NodeButton>
292+
</FileContextMenu>
211293
);
212294
}
213295

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@octokit/rest": "^21.0.2",
5959
"@octokit/types": "^13.6.2",
6060
"@openrouter/ai-sdk-provider": "^0.0.5",
61+
"@radix-ui/react-context-menu": "^2.2.2",
6162
"@radix-ui/react-dialog": "^1.1.2",
6263
"@radix-ui/react-dropdown-menu": "^2.1.2",
6364
"@radix-ui/react-separator": "^1.1.0",

pnpm-lock.yaml

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)