Skip to content

Commit 2f63a29

Browse files
committed
feat(editor): folder-tree context menu + rename in the Sources tree
Right-clicking a folder in the Content Browser's Sources tree did nothing, while the same folder as a grid tile had a full menu — so renaming or deleting a folder meant hunting for it in the grid. The tree rows now open the shared folder context menu (Open / Rename / Duplicate / Delete / Copy Path / Show in Explorer) and support F2-to-rename inline, matching the grid. Fixes a real double-editor bug this surfaced: a folder shows in BOTH the tree and the grid when cwd is its parent, so a single `renaming` path mounted TWO RenameInputs that fought for focus and immediately committed each other closed. A `renameInTree` flag now scopes the editor to the surface that started the rename, so exactly one mounts. Verified: F2 on a tree folder shows one editor in the tree (no grid duplicate), and grid rename still works unchanged.
1 parent 1ff3edf commit 2f63a29

1 file changed

Lines changed: 53 additions & 12 deletions

File tree

desktop/src/panels/ContentBrowser.tsx

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ function FolderNode({
224224
onSelect,
225225
folderDrop,
226226
dropPath,
227+
onContext,
228+
renaming,
229+
onRename,
230+
onCommitRename,
231+
onCancelRename,
227232
}: {
228233
path: string;
229234
name: string;
@@ -234,6 +239,13 @@ function FolderNode({
234239
folderPath: string,
235240
) => Pick<React.HTMLAttributes<HTMLDivElement>, 'onDragEnter' | 'onDragOver' | 'onDragLeave' | 'onDrop'>;
236241
dropPath?: string | null;
242+
/** Right-click a folder row → the shared folder context menu (Open/Rename/Delete…). */
243+
onContext?: (path: string, name: string, ev: React.MouseEvent) => void;
244+
/** The path currently being inline-renamed (shows the editor in that row). */
245+
renaming?: string | null;
246+
onRename?: (path: string) => void; // F2 → start renaming this folder
247+
onCommitRename?: (path: string, value: string) => void;
248+
onCancelRename?: () => void;
237249
}) {
238250
const [open, setOpen] = useState(depth === 0);
239251
const children = useDir(open ? path : null).entries;
@@ -248,6 +260,14 @@ function FolderNode({
248260
e.preventDefault();
249261
onSelect(path);
250262
break;
263+
case 'F2':
264+
// Rename in the tree, matching the grid — the root (depth 0 = project) has
265+
// no real folder path to rename.
266+
if (depth > 0 && onRename) {
267+
e.preventDefault();
268+
onRename(path);
269+
}
270+
break;
251271
case 'ArrowRight':
252272
e.preventDefault();
253273
e.stopPropagation();
@@ -284,6 +304,7 @@ function FolderNode({
284304
tabIndex={0}
285305
onKeyDown={onRowKey}
286306
onClick={() => onSelect(path)}
307+
onContextMenu={depth > 0 && onContext ? (ev) => onContext(path, name, ev) : undefined}
287308
{...(folderDrop ? folderDrop(path) : null)}
288309
>
289310
<span
@@ -298,10 +319,15 @@ function FolderNode({
298319
<span className="ti">
299320
<AssetIcon type="folder" size={14} />
300321
</span>
301-
<span className="tn">{name}</span>
322+
{renaming === path && onCommitRename && onCancelRename ? (
323+
<RenameInput name={name} onCommit={(v) => onCommitRename(path, v)} onCancel={onCancelRename} />
324+
) : (
325+
<span className="tn">{name}</span>
326+
)}
302327
</div>
303328
{open && subdirs.map((d) => (
304-
<FolderNode key={d.name} path={join(path, d.name)} name={d.name} depth={depth + 1} cwd={cwd} onSelect={onSelect} folderDrop={folderDrop} dropPath={dropPath} />
329+
<FolderNode key={d.name} path={join(path, d.name)} name={d.name} depth={depth + 1} cwd={cwd} onSelect={onSelect} folderDrop={folderDrop} dropPath={dropPath}
330+
onContext={onContext} renaming={renaming} onRename={onRename} onCommitRename={onCommitRename} onCancelRename={onCancelRename} />
305331
))}
306332
</>
307333
);
@@ -312,8 +338,16 @@ export function ContentBrowser() {
312338
const { cwd, go, back, forward, up, reset, canBack, canForward, canUp } = useNav();
313339
const [query, setQuery] = useState('');
314340
// A right-click menu: on an item (target set) or on empty space (target null).
315-
const [ctx, setCtx] = useState<{ x: number; y: number; target: { path: string; entry: DirEntry } | null } | null>(null);
341+
const [ctx, setCtx] = useState<{ x: number; y: number; target: { path: string; entry: DirEntry; inTree?: boolean } | null } | null>(null);
316342
const [renaming, setRenaming] = useState<string | null>(null);
343+
// A folder can appear in BOTH the tree and the grid (cwd = its parent). Track
344+
// which surface owns the rename so only ONE editor mounts — two RenameInputs on
345+
// the same path fight for focus and immediately commit each other closed.
346+
const [renameInTree, setRenameInTree] = useState(false);
347+
const startRename = (path: string, inTree = false) => {
348+
setRenameInTree(inTree);
349+
setRenaming(path);
350+
};
317351
// The tile being dragged dims so the source of the move reads at a glance.
318352
const [dragPath, setDragPath] = useState<string | null>(null);
319353
const [filters, setFilters] = useState<Set<AssetType>>(new Set());
@@ -619,7 +653,7 @@ export function ContentBrowser() {
619653
case 'F2': {
620654
if (idx >= 0 && selected) {
621655
e.preventDefault();
622-
setRenaming(selected);
656+
startRename(selected);
623657
}
624658
break;
625659
}
@@ -670,7 +704,7 @@ export function ContentBrowser() {
670704
await window.estella.fs.mkdir(path);
671705
refreshFs();
672706
selectAsset(path);
673-
setRenaming(path); // drop straight into rename, like UE5
707+
startRename(path); // drop straight into rename, like UE5
674708
} catch (e) {
675709
Toasts.push(t('cb.newFolderFailed', { error: errMsg(e) }), 'error');
676710
}
@@ -681,7 +715,7 @@ export function ContentBrowser() {
681715
const path = await ProjectStore.createSceneFile(cwd);
682716
refreshFs();
683717
selectAsset(path);
684-
setRenaming(path); // drop into rename, like New Folder
718+
startRename(path); // drop into rename, like New Folder
685719
} catch (e) {
686720
Toasts.push(t('cb.newSceneFailed', { error: errMsg(e) }), 'error');
687721
}
@@ -692,7 +726,7 @@ export function ContentBrowser() {
692726
const path = await ProjectStore.createInputMapFile(cwd);
693727
refreshFs();
694728
selectAsset(path); // unified inspector opens the input-map editor
695-
setRenaming(path);
729+
startRename(path);
696730
} catch (e) {
697731
Toasts.push(t('cb.newInputMapFailed', { error: errMsg(e) }), 'error');
698732
}
@@ -703,7 +737,7 @@ export function ContentBrowser() {
703737
const path = await ProjectStore.createLocaleTableFile(cwd);
704738
refreshFs();
705739
selectAsset(path);
706-
setRenaming(path);
740+
startRename(path);
707741
} catch (e) {
708742
Toasts.push(t('cb.newLocaleTableFailed', { error: errMsg(e) }), 'error');
709743
}
@@ -969,7 +1003,7 @@ export function ContentBrowser() {
9691003
...(isMaterial
9701004
? [{ label: t('cb.menuCreateMaterialInstance'), onClick: () => void createMaterialInstance(path) }]
9711005
: []),
972-
{ label: t('ui.rename'), onClick: () => setRenaming(path) },
1006+
{ label: t('ui.rename'), onClick: () => startRename(path, !!ctx?.target?.inTree) },
9731007
{ label: t('cb.menuDuplicate'), onClick: () => void duplicate(path) },
9741008
{ sep: true },
9751009
{ label: t('cb.menuCopyPath'), onClick: () => copy(path, t('cb.copiedPath')) },
@@ -999,7 +1033,14 @@ export function ContentBrowser() {
9991033
</div>
10001034
<div className="cb-src-body" role="tree" aria-label={t('cb.folders')}>
10011035
<div className="cb-sec">{t('cb.folders')}</div>
1002-
<FolderNode path="" name={project.name} depth={0} cwd={cwd} onSelect={go} folderDrop={folderDrop} dropPath={dropFolder} />
1036+
<FolderNode path="" name={project.name} depth={0} cwd={cwd} onSelect={go} folderDrop={folderDrop} dropPath={dropFolder}
1037+
onContext={(p, n, ev) => {
1038+
ev.preventDefault();
1039+
ev.stopPropagation();
1040+
setCtx({ x: ev.clientX, y: ev.clientY, target: { path: p, entry: { name: n, isDir: true }, inTree: true } });
1041+
}}
1042+
renaming={renameInTree ? renaming : null} onRename={(p) => startRename(p, true)}
1043+
onCommitRename={(p, v) => void commitRename(p, v)} onCancelRename={() => setRenaming(null)} />
10031044
</div>
10041045
</div>
10051046

@@ -1177,7 +1218,7 @@ export function ContentBrowser() {
11771218
className="nm"
11781219
style={it.isDir ? undefined : ({ ['--tc' as string]: assetTint(type) } as React.CSSProperties)}
11791220
>
1180-
{renaming === path ? (
1221+
{renaming === path && !renameInTree ? (
11811222
<RenameInput
11821223
name={it.name}
11831224
onCommit={(v) => void commitRename(path, v)}
@@ -1221,7 +1262,7 @@ export function ContentBrowser() {
12211262
>
12221263
<span className="ln">
12231264
<AssetIcon type={type} size={15} />
1224-
{renaming === path ? (
1265+
{renaming === path && !renameInTree ? (
12251266
<RenameInput
12261267
name={it.name}
12271268
onCommit={(v) => void commitRename(path, v)}

0 commit comments

Comments
 (0)