mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(app): resolve Changes file actions from the repository root
Diff paths are relative to the worktree root, but the Changes pane resolved them against the workspace directory. For a project in a repository subfolder, that repeated the subfolder in the path, so Copy path was wrong and Open file, Download, Duplicate, and Reveal failed. Closes #8125 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
da8d58bfee
commit
cacd97d663
@@ -16,6 +16,7 @@ interface DirtyWorkspace {
|
||||
}
|
||||
|
||||
interface WorkspaceFixtureOptions {
|
||||
workspaceSubdirectory?: string;
|
||||
includeDeletedFile?: boolean;
|
||||
includeNestedFolders?: boolean;
|
||||
includeRenamedFile?: boolean;
|
||||
@@ -435,6 +436,76 @@ test("changes file actions open below the right-click without a reserved kebab",
|
||||
await expect(page.getByTestId("workspace-tab-file_src/use-mounted-tab-set.ts")).toBeVisible();
|
||||
});
|
||||
|
||||
for (const { workspaceSubdirectory, filePath, content, visibleContent } of [
|
||||
{
|
||||
workspaceSubdirectory: ".",
|
||||
filePath: "src/use-mounted-tab-set.ts",
|
||||
content: AFTER,
|
||||
visibleContent: "interface DeriveRenderMountedTabIdsInput",
|
||||
},
|
||||
{
|
||||
workspaceSubdirectory: "src",
|
||||
filePath: "src/use-mounted-tab-set.ts",
|
||||
content: AFTER,
|
||||
visibleContent: "interface DeriveRenderMountedTabIdsInput",
|
||||
},
|
||||
{
|
||||
workspaceSubdirectory: "src",
|
||||
filePath: "a-root-note.txt",
|
||||
content: "root note\n",
|
||||
visibleContent: "root note",
|
||||
},
|
||||
]) {
|
||||
test(`changes file actions resolve ${filePath} from project ${workspaceSubdirectory}`, async ({
|
||||
page,
|
||||
context,
|
||||
}, testInfo) => {
|
||||
const workspace = await createWorkspaceWithMountedTabDiff({
|
||||
workspaceSubdirectory,
|
||||
includeRootFileSortingFirst: filePath === "a-root-note.txt",
|
||||
});
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
await openWorkspaceChanges(page, workspace);
|
||||
const panel = page.getByTestId("working-diff-panel").filter({ visible: true });
|
||||
const header = diffHeaderForPath(panel, filePath).getByTestId(/^diff-file-\d+-toggle$/);
|
||||
await header.click({ button: "right" });
|
||||
await testInfo.attach("changes-file-menu", {
|
||||
body: await page.screenshot(),
|
||||
contentType: "image/png",
|
||||
});
|
||||
await test.step("Duplicate creates a copy next to the source", async () => {
|
||||
const file = path.parse(filePath);
|
||||
const copyPath = path.join(workspace.repoPath, file.dir, `${file.name} copy${file.ext}`);
|
||||
await page.getByRole("menuitem", { name: "Duplicate", exact: true }).click();
|
||||
await expect
|
||||
.poll(() => readFileIfPresent(copyPath), { message: `Expected duplicate at ${copyPath}` })
|
||||
.toBe(content);
|
||||
});
|
||||
await header.click({ button: "right" });
|
||||
await page.getByRole("menuitem", { name: "Copy relative path", exact: true }).click();
|
||||
await expectClipboardText(page, filePath);
|
||||
await header.click({ button: "right" });
|
||||
await page.getByRole("menuitem", { name: "Copy path", exact: true }).click();
|
||||
await expectClipboardText(page, path.join(workspace.repoPath, filePath));
|
||||
|
||||
await header.click({ button: "right" });
|
||||
const downloaded = page.waitForEvent("download");
|
||||
await page.getByRole("menuitem", { name: "Download", exact: true }).click();
|
||||
const download = await downloaded;
|
||||
expect(download.suggestedFilename()).toBe(path.basename(filePath));
|
||||
expect(await readFile((await download.path())!, "utf8")).toBe(content);
|
||||
|
||||
await header.click({ button: "right" });
|
||||
await page.getByRole("menuitem", { name: "Open file", exact: true }).click();
|
||||
const editor = page.getByTestId("workspace-file-pane").filter({ visible: true });
|
||||
await expect(editor.locator(".cm-content")).toContainText(visibleContent);
|
||||
await testInfo.attach("opened-changed-file", {
|
||||
body: await page.screenshot(),
|
||||
contentType: "image/png",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("canvas file headers select without toggling for context menu and long press", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -1591,7 +1662,7 @@ async function createWorkspaceWithMountedTabDiff(
|
||||
);
|
||||
}
|
||||
const createdWorkspace = await client.createWorkspace({
|
||||
source: { kind: "directory", path: repo.path },
|
||||
source: { kind: "directory", path: path.join(repo.path, options.workspaceSubdirectory ?? ".") },
|
||||
});
|
||||
if (!createdWorkspace.workspace) {
|
||||
throw new Error(createdWorkspace.error ?? `Failed to create workspace ${repo.path}`);
|
||||
|
||||
@@ -56,6 +56,7 @@ import * as Clipboard from "expo-clipboard";
|
||||
import { useFileDownload } from "@/hooks/use-file-download";
|
||||
import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
|
||||
import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths";
|
||||
import { isWorkspaceRootPath } from "@/workspace/file-open";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { GitActionsSplitButton } from "@/git/actions-split-button";
|
||||
import type { GitActions } from "@/git/policy";
|
||||
@@ -1452,6 +1453,10 @@ function useDiffTabNavigation({
|
||||
};
|
||||
}
|
||||
|
||||
function resolveChangesFileRoot(status: CheckoutStatusPayload | null, cwd: string): string {
|
||||
return status?.repoRoot ?? cwd;
|
||||
}
|
||||
|
||||
export function ChangesSurface({
|
||||
serverId,
|
||||
workspaceId,
|
||||
@@ -1631,14 +1636,30 @@ export function ChangesSurface({
|
||||
}),
|
||||
[appSettings.monoFontFamily, codeFontSize, effectiveLayout, wrapLines],
|
||||
);
|
||||
const downloadFile = useFileDownload({ serverId, workspaceId, workspaceRoot: cwd });
|
||||
const fileRoot = resolveChangesFileRoot(status, cwd);
|
||||
const isRootWorkspace = isWorkspaceRootPath({ path: fileRoot, workspaceRoot: cwd });
|
||||
const resolveFilePath = useCallback(
|
||||
(path: string) => buildAbsoluteExplorerPath({ workspaceRoot: fileRoot, entryPath: path }),
|
||||
[fileRoot],
|
||||
);
|
||||
const resolveFileTargetPath = useCallback(
|
||||
(path: string) => (isRootWorkspace ? path : resolveFilePath(path)),
|
||||
[isRootWorkspace, resolveFilePath],
|
||||
);
|
||||
const handleOpenFile = useCallback(
|
||||
(path: string) => onOpenFile?.(resolveFileTargetPath(path)),
|
||||
[onOpenFile, resolveFileTargetPath],
|
||||
);
|
||||
const handleAddToChat = useCallback(
|
||||
(path: string) => onAddToChat?.(resolveFileTargetPath(path)),
|
||||
[onAddToChat, resolveFileTargetPath],
|
||||
);
|
||||
const downloadFile = useFileDownload({ serverId, workspaceId, workspaceRoot: fileRoot });
|
||||
const handleCopyPath = useCallback(
|
||||
(path: string) => {
|
||||
void Clipboard.setStringAsync(
|
||||
buildAbsoluteExplorerPath({ workspaceRoot: cwd, entryPath: path }),
|
||||
);
|
||||
void Clipboard.setStringAsync(resolveFilePath(path));
|
||||
},
|
||||
[cwd],
|
||||
[resolveFilePath],
|
||||
);
|
||||
const handleCopyRelativePath = useCallback((path: string) => {
|
||||
void Clipboard.setStringAsync(path);
|
||||
@@ -1652,7 +1673,7 @@ export function ChangesSurface({
|
||||
await openDesktopTarget({
|
||||
editorId: fileManagerTarget.id,
|
||||
workspacePath: cwd,
|
||||
filePath: buildAbsoluteExplorerPath({ workspaceRoot: cwd, entryPath: path }),
|
||||
filePath: resolveFilePath(path),
|
||||
});
|
||||
} catch (cause) {
|
||||
toast.error(
|
||||
@@ -1660,7 +1681,7 @@ export function ChangesSurface({
|
||||
);
|
||||
}
|
||||
},
|
||||
[cwd, fileManagerTarget, t, toast],
|
||||
[cwd, fileManagerTarget, resolveFilePath, t, toast],
|
||||
);
|
||||
const handleDownloadPath = useCallback(
|
||||
(path: string) => {
|
||||
@@ -1674,7 +1695,7 @@ export function ChangesSurface({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = await client.duplicateFileEntry({ cwd, path });
|
||||
const payload = await client.duplicateFileEntry({ cwd: fileRoot, path });
|
||||
if (!payload.success) {
|
||||
toast.error(payload.error ?? t("workspace.fileExplorer.errors.duplicateFailed"));
|
||||
}
|
||||
@@ -1682,7 +1703,7 @@ export function ChangesSurface({
|
||||
toast.error(cause instanceof Error ? cause.message : String(cause));
|
||||
}
|
||||
},
|
||||
[client, cwd, t, toast],
|
||||
[client, fileRoot, t, toast],
|
||||
);
|
||||
const onRevertPath = useDiscardChangesAction({ serverId, cwd, diffMode });
|
||||
const [localFocusRequest, setLocalFocusRequest] = useState<{
|
||||
@@ -1718,9 +1739,9 @@ export function ChangesSurface({
|
||||
focusPath: documentFocusRequest?.path,
|
||||
focusRequestId: documentFocusRequest?.revision,
|
||||
workspaceFileDragScope: workspaceId ? { serverId, workspaceId } : undefined,
|
||||
onOpenFile,
|
||||
onOpenFile: onOpenFile ? handleOpenFile : undefined,
|
||||
onOpenToSide,
|
||||
onAddToChat,
|
||||
onAddToChat: onAddToChat ? handleAddToChat : undefined,
|
||||
onCopyPath: handleCopyPath,
|
||||
onCopyRelativePath: handleCopyRelativePath,
|
||||
onReveal: fileManagerTarget ? handleRevealPath : undefined,
|
||||
@@ -1736,8 +1757,10 @@ export function ChangesSurface({
|
||||
serverId,
|
||||
workspaceId,
|
||||
onOpenFile,
|
||||
handleOpenFile,
|
||||
onOpenToSide,
|
||||
onAddToChat,
|
||||
handleAddToChat,
|
||||
handleCopyPath,
|
||||
handleCopyRelativePath,
|
||||
handleDownloadPath,
|
||||
|
||||
Reference in New Issue
Block a user