feat: file browser context menu, rename, delete-to-trash, and drag-to-move - #355
Open
lyhue1991 wants to merge 1 commit into
Open
feat: file browser context menu, rename, delete-to-trash, and drag-to-move#355lyhue1991 wants to merge 1 commit into
lyhue1991 wants to merge 1 commit into
Conversation
Reference JupyterLab's filebrowser. Single-file/folder operations on the
existing recursive tree (multi-select left for later).
Frontend (components/):
- FileContextMenu: right-click menu (rename, move to trash, new file/folder,
download, copy path); clamps to viewport, closes on outside click/ESC/scroll
- InlineFileNameInput: inline editor; selects name-without-extension,
commits on Enter/blur, cancels on Escape
- FileExplorer: context menu wiring, inline rename, delete-to-trash dialog,
HTML5 drag-to-move (folder-only drop targets, rejects self/descendant),
new file/folder via phantom input row, expandedPaths remap on rename/move,
error/notice toasts
Backend (app/api/files/[...path]/route.ts):
- DELETE -> move to trash via 'trash' npm package
- PATCH {to} -> rename/move: basename validation, realpath root checks on
source and target parent, isAncestorOrSelf guard, 409 on collision,
EXDEV fallback to cpSync+rmSync
- POST ?type=create -> new empty file (wx flag) / folder (mkdir), 409 on collision
- All handlers gated by isApiRequestAllowed + realpath allow-list
(blocks symlink escape, consistent with existing upload path)
Shared (lib/file-ops.ts):
- FileOpError, validateSingleFileName, isAncestorOrSelf, authorizeExistingPath
- lib/file-ops.test.mjs: 8 unit tests (node:test + jiti)
i18n: 15 new files.* keys in en.ts and zh-CN.ts
Dep: add trash@^10.1.1
There was a problem hiding this comment.
Pull request overview
This PR expands the existing recursive FileExplorer into a more full-featured file browser (inspired by JupyterLab), adding a right-click context menu and inline workflows for rename, create, delete-to-trash, and drag-to-move, backed by new server-side file operation endpoints with allowed-root + realpath security checks.
Changes:
- Add UI primitives (
FileContextMenu,InlineFileNameInput) and wire them intoFileExplorerfor context actions, inline editing, delete confirmation, and HTML5 drag-to-move. - Add backend file operations to
/api/files/[...path]for create (POST), move/rename (PATCH), and trash delete (DELETE), plus shared helpers inlib/file-ops.tsand tests. - Add i18n strings and the
trashdependency to support “move to trash” cross-platform.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds trash dependency for delete-to-trash. |
| package-lock.json | Locks trash and transitive dependencies. |
| lib/i18n/messages/en.ts | Adds files.* strings for new file operations UI. |
| lib/i18n/messages/zh-CN.ts | Adds files.* strings for new file operations UI. |
| lib/file-ops.ts | Introduces shared helpers/errors for path authorization and validations. |
| lib/file-ops.test.mjs | Adds unit tests for file-op validation + symlink/realpath authorization. |
| components/InlineFileNameInput.tsx | New inline rename/new-item input with Enter/blur commit + ESC cancel. |
| components/FileContextMenu.tsx | New context menu component with viewport clamping + dismissal behaviors. |
| components/FileExplorer.tsx | Adds context menu, rename/create/delete flows, drag-to-move, and toasts/notices. |
| app/api/files/[...path]/route.ts | Adds DELETE (trash), PATCH (rename/move), POST type=create endpoints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+315
to
+322
| try { | ||
| await trash(realPath); | ||
| } catch (error) { | ||
| return NextResponse.json( | ||
| { error: error instanceof Error ? error.message : String(error) }, | ||
| { status: 500 }, | ||
| ); | ||
| } |
Comment on lines
+387
to
+397
| try { | ||
| fs.renameSync(realSource, realTarget); | ||
| } catch (error) { | ||
| // Cross-device rename (e.g. tmpfs -> disk). Fall back to copy + remove. | ||
| if ((error as NodeJS.ErrnoException).code === "EXDEV") { | ||
| fs.cpSync(realSource, realTarget, { recursive: true }); | ||
| fs.rmSync(realSource, { recursive: true, force: true }); | ||
| } else { | ||
| throw error; | ||
| } | ||
| } |
Comment on lines
+1238
to
+1242
| items.push({ | ||
| key: "download", | ||
| label: t("files.download"), | ||
| onClick: () => window.open(`/api/files/${encodeFilePathForApi(node.fullPath)}?type=download`, "_blank"), | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
参考 JupyterLab filebrowser,在现有
FileExplorer递归树上实现单文件/单文件夹的右键菜单、重命名、删除、拖拽移动、新建。多选留作后续。改动
前端(
components/)FileContextMenu(新):右键菜单,项随节点类型生成(重命名、移到回收站、新建文件/文件夹、下载、复制路径);视口内夹紧定位,外部点击 / ESC / 滚动 / 失焦关闭。InlineFileNameInput(新):内联文件名编辑器,提交时选中「不含扩展名」部分(lastIndexOf('.')),Enter / blur 提交、ESC 取消,blur-after-Enter 用settledRef防双提交。FileExplorer:右键菜单接线、内联重命名(span↔input 切换)、删除确认对话框、HTML5 拖拽移动(仅文件夹接受 drop,拒绝拖入自身/子孙,用 ref 绕开dataTransfer在 dragover 不可读的限制)、新建文件/文件夹(幽灵输入行 + 高亮新项)、rename/move 后expandedPaths重映射、file-op 错误/通知 toast。后端(
app/api/files/[...path]/route.ts)DELETE→ 移到回收站(trash包,平台回收站命令)。PATCH {to}→ 重命名/移动:basename 校验 + 源与目标父目录 realpath 安全校验 +isAncestorOrSelf拒绝移入自身/子孙 + 409 不覆盖 + EXDEV 跨设备回退cpSync+rmSync。POST ?type=create→ 新建空文件(wxflag)/ 文件夹(mkdir),409 不覆盖。isApiRequestAllowed(request)受信校验,写操作一律走 realpath + root allow-list(与现有 upload 一致,防软链逃逸)。共享(
lib/file-ops.ts,新)FileOpError、validateSingleFileName、isAncestorOrSelf、authorizeExistingPath。lib/file-ops.test.mjs:8 个单测(node:test+ jiti),覆盖名字校验、祖先判断(含 name-prefix-not-ancestor 边界)、realpath 软链逃逸拒绝 / 404 / 指回 root 内的软链放行。i18n
files.*键(en.ts+zh-CN.ts)。依赖
trash@^10.1.1(ESM,壳出平台回收站命令)。验证
tsc --noEmit✅npm run lint✅(0 errors / 0 warnings)node --test lib/file-ops.test.mjs✅(8/8)node --test lib/i18n/{format,registry}.test.mjs✅范围外(后续)
多选、Ctrl-拖拽复制、移动覆盖确认、回收站依赖换平台原生命令实现。
手动测试清单