Skip to content

feat: file browser context menu, rename, delete-to-trash, and drag-to-move - #355

Closed
lyhue1991 wants to merge 2 commits into
agegr:mainfrom
lyhue1991:feat/file-browser-context-menu-dnd
Closed

feat: file browser context menu, rename, delete-to-trash, and drag-to-move#355
lyhue1991 wants to merge 2 commits into
agegr:mainfrom
lyhue1991:feat/file-browser-context-menu-dnd

Conversation

@lyhue1991

@lyhue1991 lyhue1991 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

参考 JupyterLab filebrowser,在现有 FileExplorer 递归树上实现单文件/单文件夹的右键菜单、重命名、删除、拖拽移动、新建。多选留作后续。

更新:已 rebase 到最新 main(v0.8.7 / pi v0.84.0),验证通过。本 PR 现含 2 个提交:功能主体 + trash webpack 外部化修复。

改动

前端(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 → 新建空文件(wx flag)/ 文件夹(mkdir),409 不覆盖。
  • 所有 handler 均先过 isApiRequestAllowed(request) 受信校验,写操作一律走 realpath + root allow-list(与现有 upload 一致,防软链逃逸)。

共享(lib/file-ops.ts,新)

  • FileOpErrorvalidateSingleFileNameisAncestorOrSelfauthorizeExistingPath
  • lib/file-ops.test.mjs:8 个单测(node:test + jiti),覆盖名字校验、祖先判断(含 name-prefix-not-ancestor 边界)、realpath 软链逃逸拒绝 / 404 / 指回 root 内的软链放行。

构建(next.config.ts

  • trash 加入 serverExternalPackagestrash 是纯 ESM 包,webpack 内联打包会破坏其内部路径处理,导致 DELETE 在生产构建下报 The "path" argument must be of type string... Received an instance of URL;改为运行时 import("trash") 后正常。

i18n

  • 新增 15 个 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 ✅(6/6)
  • 生产构建(next build --webpack)下端到端 DELETE 验证 ✅

范围外(后续)

多选、Ctrl-拖拽复制、移动覆盖确认、回收站依赖换平台原生命令实现。

手动测试清单

  • 右键重命名(Enter 提交 / ESC 取消 / 扩展名不选中)
  • 删除到回收站(系统回收站可还原)
  • 新建文件 / 文件夹
  • 拖文件入文件夹
  • 拖文件夹入另一文件夹
  • 拖入自身 / 子孙被拒
  • 目标已存在 → 409 提示

Copilot AI review requested due to automatic review settings August 2, 2026 13:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 into FileExplorer for 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 in lib/file-ops.ts and tests.
  • Add i18n strings and the trash dependency 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 thread app/api/files/[...path]/route.ts
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"),
});
@lyhue1991

lyhue1991 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

效果预览:
image

lyhue1991 added 2 commits August 7, 2026 07:49
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
trash is ESM-only. webpack inlined it into the server bundle, corrupting
its internal path handling and making DELETE /api/files fail with
"The 'path' argument must be of type string or an instance of URL.
Received an instance of URL".

Adding 'trash' to serverExternalPackages keeps it as a runtime
import('trash') from node_modules, preserving its native ESM form.
Verified end-to-end: production build now moves files to trash.
@lyhue1991
lyhue1991 force-pushed the feat/file-browser-context-menu-dnd branch from a743b0a to 2511184 Compare August 6, 2026 23:49
@agegr

agegr commented Aug 7, 2026

Copy link
Copy Markdown
Owner

产品方向上,我更希望 pi-web 保持简单,定位在会话交互和轻量文件查看,而不是逐步扩展成完整的文件管理器。右键菜单、新建、重命名、删除和拖拽移动会明显增加 UI、后端维护成本以及误操作风险,同时右键入口对触屏设备也不友好。

@agegr agegr closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants