feat(files): move files and folders across directories in the stack explorer (#1373)

* feat(files): move files and folders across directories in the stack explorer

Add a cross-directory move to the stack file explorer. Files and folders can
be relocated either through a "Move to..." context-menu item that opens a
folder-picker dialog, or by dragging an entry onto a folder node (or onto the
root area to move it to the stack root).

The backend reuses the existing rename endpoint: renameStackPath now resolves
both ends through the leaf helper, so a symlink moves as the link entry rather
than its target, and it guards against moving a directory into its own subtree.
A cross-filesystem rename surfaces as a clean 409 instead of a 500. Protected
root files (compose / docker-compose / .env) stay put. Moving the open file, or
a folder containing it, deselects the viewer; a move that would discard unsaved
edits is blocked with a clear message.

* fix(files): fold case in move guards and keep the move dialog open on failure

Harden the cross-directory move against case-insensitive filesystems and fix a
dialog dismissal edge:

- Protected root files (compose / docker-compose / .env) were gated by an exact,
  lowercase name match. On a case-insensitive filesystem a request like
  COMPOSE.YAML resolves to the real compose.yaml and slipped past the gate, so a
  protected file could be moved out of the stack root via the API. The gate now
  folds case on case-insensitive platforms; Linux stays case-sensitive, where a
  differently-cased name is a distinct, unprotected file.
- The directory-into-descendant guard compared resolved paths case-sensitively,
  so a source supplied with non-disk casing skipped the guard and fell through to
  an opaque OS error (500) instead of a clean 400. The comparison now folds case
  the same way.
- The move dialog closed after awaiting the move regardless of outcome, so a
  blocked move (unsaved edits) or a failed move dismissed the picker as if it had
  succeeded. The shared handler now reports success and the dialog only closes on
  an actual move.
This commit is contained in:
Anso
2026-06-14 21:56:06 -04:00
committed by GitHub
parent 0066887cee
commit 888f658a7a
16 changed files with 1204 additions and 45 deletions
+5 -1
View File
@@ -1703,7 +1703,8 @@ type FsErrorCode =
| 'FILE_EXISTS'
| 'DIR_EXISTS'
| 'PROTECTED_FILE'
| 'LINK_CHMOD_UNSUPPORTED';
| 'LINK_CHMOD_UNSUPPORTED'
| 'EXDEV';
function sendFsError(
res: Response,
@@ -1730,6 +1731,9 @@ function sendFsError(
if (e.code === 'EEXIST') {
return res.status(409).json({ error: e.message, code: 'ALREADY_EXISTS' satisfies FsErrorCode });
}
if (e.code === 'EXDEV') {
return res.status(409).json({ error: 'Cannot move across a storage boundary', code: 'EXDEV' satisfies FsErrorCode });
}
if (e.code === 'ENOTDIR') {
return res.status(400).json({ error: 'Target path is not a directory', code: 'INVALID_PATH' satisfies FsErrorCode });
}