Compare commits

...
8 changed files with 69 additions and 17 deletions

No files matched your search

+1 -1
View File
@@ -37,7 +37,7 @@ try {
const one = await open("");
const tree = await one.evaluate(() => ({
folders: [...document.querySelectorAll(".workbench-change-folder")].length,
rows: [...document.querySelectorAll('.workbench-change-row[data-action="workbench.diff"]')].length,
rows: [...document.querySelectorAll('.workbench-change-row[data-action-click="workbench.diff"]')].length,
overflow: document.querySelector(".workbench-explorer").scrollWidth > document.querySelector(".workbench-explorer").clientWidth + 1,
}));
check(`${tag}: the changes are a folder tree`, tree.folders >= 2 && tree.rows === 4, JSON.stringify(tree));
@@ -1,4 +1,4 @@
import { useMemo, useState, type ReactNode } from "react";
import { useMemo, useState, type MouseEvent, type ReactNode } from "react";
import { t } from "../i18n";
import { reason } from "../i18n/kernel";
import type { AgentPort, StageRequest, WorkspaceChange } from "../port/port";
@@ -15,6 +15,8 @@ interface Props {
// The path whose diff is on screen, "" when none is.
current: string;
onOpen: (path: string) => void;
// A right-click or the context-menu key on a row; the path is workspace-relative.
onMenu?: (e: MouseEvent<HTMLElement>, path: string, folder: boolean) => void;
// Asked for when staging or discarding changed the tree. Absent means this
// surface cannot refresh the list, so it offers neither.
onChanged?: () => void;
@@ -37,7 +39,7 @@ const withRepo = (repo: string | undefined, req: StageRequest): StageRequest =>
// What differs as a folder tree, without opening anything, and which of it the next
// commit will record: the index is what a commit reads, so it is shown and changed
// here, a folder at a time or a file at a time. A change can also be thrown away.
export function WorkbenchChanges({ port, workspaceRoot, changes, repo, current, onOpen, onChanged, commit, actions, below }: Props) {
export function WorkbenchChanges({ port, workspaceRoot, changes, repo, current, onOpen, onMenu, onChanged, commit, actions, below }: Props) {
const [busy, setBusy] = useState(false);
const [failure, setFailure] = useState("");
const [shut, setShut] = useState<Set<string>>(new Set());
@@ -84,8 +86,9 @@ export function WorkbenchChanges({ port, workspaceRoot, changes, repo, current,
<StudioIcon name="check" aria-hidden="true" />
</button>
)}
<button type="button" className="workbench-change-row workbench-folder-row" style={inset} data-action="workbench.change-folder" data-target={node.path}
<button type="button" className="workbench-change-row workbench-folder-row" style={inset} data-action-click="workbench.change-folder" data-target={node.path}
{...workspaceDrag(workspaceRoot, repo ? `${repo}/${node.path}` : node.path)}
data-action-contextmenu={onMenu ? "workbench.menu" : undefined} onContextMenu={onMenu && ((e) => onMenu(e, repo ? `${repo}/${node.path}` : node.path, true))}
aria-expanded={open} onClick={() => fold(node.path)}>
<StudioIcon name={open ? "down" : "chevron"} aria-hidden="true" />
<span>{node.name}</span>
@@ -110,8 +113,9 @@ export function WorkbenchChanges({ port, workspaceRoot, changes, repo, current,
<StudioIcon name="check" aria-hidden="true" />
</button>
)}
<button className="workbench-change-row" style={inset} data-action="workbench.diff" data-target={item.path} title={item.path}
<button className="workbench-change-row" style={inset} data-action-click="workbench.diff" data-target={item.path} title={item.path}
{...workspaceDrag(workspaceRoot, repo ? `${repo}/${item.path}` : item.path)}
data-action-contextmenu={onMenu ? "workbench.menu" : undefined} onContextMenu={onMenu && ((e) => onMenu(e, repo ? `${repo}/${item.path}` : item.path, false))}
aria-current={current === item.path ? "page" : undefined} onClick={() => onOpen(item.path)}>
<em data-status={item.status}>{item.status || "M"}</em>
<span>{node.name}</span>
+11 -9
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, type MouseEvent } from "react";
import { t } from "../i18n";
import type { AgentPort, WorkspaceChange } from "../port/port";
import type { GitRepoRow } from "../port/workspace";
@@ -24,6 +24,7 @@ interface Props {
// The workspace-relative path whose diff is on screen, "" when none is.
current: string;
onOpen: (path: string) => void;
onMenu?: (e: MouseEvent<HTMLElement>, path: string, folder: boolean) => void;
onChanged?: () => void;
remote: boolean;
}
@@ -34,7 +35,7 @@ const join = (repo: string, path: string) => (repo ? `${repo}/${path}` : path);
// stage, discard and commit, the push of the branch, and its history. A workspace
// that is one repository shows just that; a folder of projects shows one section per
// repository, each acting on its own.
export function WorkbenchGit({ port, workspaceRoot, changes, repos, failure = "", onRetry, wrote, current, onOpen, onChanged, remote }: Props) {
export function WorkbenchGit({ port, workspaceRoot, changes, repos, failure = "", onRetry, wrote, current, onOpen, onMenu, onChanged, remote }: Props) {
// A list that could not be read says so, and the workspace is shown as the one
// repository it was before there was a list: the changes the pane holds are real.
if (failure) {
@@ -44,25 +45,25 @@ export function WorkbenchGit({ port, workspaceRoot, changes, repos, failure = ""
{failure}
{onRetry && <button type="button" className="btn sm" data-action="workbench.repos-reload" onClick={onRetry}>{t("重新读取")}</button>}
</p>
<RepoSection port={port} workspaceRoot={workspaceRoot} repo="" changes={changes} current={current} onOpen={onOpen} onChanged={onChanged} remote={remote} />
<RepoSection port={port} workspaceRoot={workspaceRoot} repo="" changes={changes} current={current} onOpen={onOpen} onMenu={onMenu} onChanged={onChanged} remote={remote} />
</>
);
}
if (repos.length === 0) return null;
const several = repos.length > 1 || repos[0].dir !== "";
if (!several) {
return <RepoSection port={port} workspaceRoot={workspaceRoot} repo="" changes={changes} current={current} onOpen={onOpen} onChanged={onChanged} remote={remote} />;
return <RepoSection port={port} workspaceRoot={workspaceRoot} repo="" changes={changes} current={current} onOpen={onOpen} onMenu={onMenu} onChanged={onChanged} remote={remote} />;
}
return (
<div className="workbench-git" data-repos={repos.length}>
{repos.map((row) => (
<Folder key={row.dir} port={port} workspaceRoot={workspaceRoot} row={row} wrote={wrote} current={current} onOpen={onOpen} remote={remote} />
<Folder key={row.dir} port={port} workspaceRoot={workspaceRoot} row={row} wrote={wrote} current={current} onOpen={onOpen} onMenu={onMenu} remote={remote} />
))}
</div>
);
}
function Folder({ port, workspaceRoot, row, wrote, current, onOpen, remote }: { port: AgentPort; workspaceRoot?: string; row: GitRepoRow; wrote: number; current: string; onOpen: (path: string) => void; remote: boolean }) {
function Folder({ port, workspaceRoot, row, wrote, current, onOpen, onMenu, remote }: { port: AgentPort; workspaceRoot?: string; row: GitRepoRow; wrote: number; current: string; onOpen: (path: string) => void; onMenu?: Props["onMenu"]; remote: boolean }) {
const [open, setOpen] = useState(true);
const { changes, reload } = useRepoChanges(port, row.dir, open, wrote);
return (
@@ -74,13 +75,13 @@ function Folder({ port, workspaceRoot, row, wrote, current, onOpen, remote }: {
<span>{row.branch}</span>
<small>{changes.length}</small>
</button>
{open && <RepoSection port={port} workspaceRoot={workspaceRoot} repo={row.dir} changes={changes} current={current} onOpen={onOpen} onChanged={reload} remote={remote} />}
{open && <RepoSection port={port} workspaceRoot={workspaceRoot} repo={row.dir} changes={changes} current={current} onOpen={onOpen} onMenu={onMenu} onChanged={reload} remote={remote} />}
</section>
);
}
function RepoSection({ port, workspaceRoot, repo, changes, current, onOpen, onChanged, remote }: {
port: AgentPort; workspaceRoot?: string; repo: string; changes: WorkspaceChange[]; current: string; onOpen: (path: string) => void; onChanged?: () => void; remote: boolean;
function RepoSection({ port, workspaceRoot, repo, changes, current, onOpen, onMenu, onChanged, remote }: {
port: AgentPort; workspaceRoot?: string; repo: string; changes: WorkspaceChange[]; current: string; onOpen: (path: string) => void; onMenu?: Props["onMenu"]; onChanged?: () => void; remote: boolean;
}) {
const refresh = () => onChanged?.();
const commit = useCommitCard(port, refresh, repo || undefined);
@@ -95,6 +96,7 @@ function RepoSection({ port, workspaceRoot, repo, changes, current, onOpen, onCh
changes={changes}
current={inside}
onOpen={(path) => onOpen(join(repo, path))}
onMenu={onMenu}
onChanged={onChanged}
commit={commit}
actions={remote ? null : push.button}
@@ -690,6 +690,7 @@ export function WorkbenchPanel({
openFile(path);
setMode("diff");
}}
onMenu={offerMenu}
onChanged={onTreeChanged}
remote={remote}
/>
@@ -46,6 +46,6 @@ describe("a workspace with very many changes", () => {
it("draws a short list whole, with nothing left out", () => {
const { container } = draw(many(30));
expect(screen.queryByRole("button", { name: /再显示/ })).toBeNull();
expect(container.querySelectorAll('[data-action="workbench.diff"]').length).toBe(30);
expect(container.querySelectorAll('[data-action-click="workbench.diff"]').length).toBe(30);
});
});
@@ -0,0 +1,43 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, expect, it, vi } from "vitest";
import { MockPort } from "../port/mock";
import type { WorkspaceChange } from "../port/port";
import { WorkbenchPanel } from "./WorkbenchPanel";
import "./testkit";
const change = (path: string, status = "M"): WorkspaceChange => ({ path, status, staged: false, unstaged: true });
const props = (port: MockPort, changes: WorkspaceChange[]) => ({
port, workspaceRoot: "/work/project", tabs: [], manual: false, shown: true, scheme: "light" as const,
changes, onCloseManual: vi.fn(), onSurfaces: vi.fn(), onExternal: vi.fn(), onAddToChat: vi.fn(),
});
afterEach(() => { cleanup(); vi.restoreAllMocks(); });
it("offers the file menu on a changed file, with the path the tree would give", async () => {
const port = new MockPort();
vi.spyOn(port, "workspaceFiles").mockResolvedValue({ files: [], directories: [] });
render(<WorkbenchPanel {...props(port, [change("web/src/prototype.html", "A")])} />);
const row = await screen.findByTitle("web/src/prototype.html");
fireEvent.contextMenu(row);
expect(await screen.findByRole("menuitem", { name: "复制路径" })).toBeTruthy();
expect(screen.getByRole("menuitem", { name: "复制引用" })).toBeTruthy();
});
it("reveals the changed file in the file manager from its menu", async () => {
const port = new MockPort();
vi.spyOn(port, "workspaceFiles").mockResolvedValue({ files: [], directories: [] });
vi.spyOn(port, "revealsFiles").mockReturnValue(true);
const reveal = vi.spyOn(port, "revealInFileManager").mockResolvedValue(undefined);
render(<WorkbenchPanel {...props(port, [change("a/b.ts")])} />);
fireEvent.contextMenu(await screen.findByTitle("a/b.ts"));
fireEvent.click(await screen.findByRole("menuitem", { name: /在访达中显示|在文件资源管理器中显示|在系统文件管理器中显示/ }));
await waitFor(() => expect(reveal).toHaveBeenCalledWith("a/b.ts"));
});
it("leaves a plain click on a changed file opening its diff", async () => {
const port = new MockPort();
vi.spyOn(port, "workspaceFiles").mockResolvedValue({ files: [], directories: [] });
render(<WorkbenchPanel {...props(port, [change("a/b.ts")])} />);
fireEvent.click(await screen.findByTitle("a/b.ts"));
expect(screen.queryByRole("menuitem", { name: "复制路径" })).toBeNull();
});
@@ -84,7 +84,7 @@ it("references search results and changed files through the same intake", async
return found!;
});
drag(row, box);
drag(document.querySelector<HTMLElement>('[data-action="workbench.diff"]')!, box);
drag(document.querySelector<HTMLElement>('[data-action-click="workbench.diff"]')!, box);
await waitFor(() => expect(screen.getAllByRole("button", { name: "移除 retry.go" })).toHaveLength(2));
expect(dropRefs.mock.calls).toEqual([[["/workspace/internal/provider/retry.go"]], [["/workspace/internal/provider/retry.go"]]]);
});
@@ -95,7 +95,7 @@ it("keeps the nested repository prefix when dragging changed files", async () =>
port.repoChanges = { "apps/site": [{ path: "README.md", status: "M" }] };
});
const row = await waitFor(() => {
const found = document.querySelector<HTMLElement>('[data-repo="apps/site"] [data-action="workbench.diff"]');
const found = document.querySelector<HTMLElement>('[data-repo="apps/site"] [data-action-click="workbench.diff"]');
expect(found).toBeTruthy();
return found!;
});
@@ -0,0 +1,2 @@
- 修复:工作台「改动」列表里的文件和文件夹现在也能右键,菜单与文件树一致,可在系统文件管理器中显示、复制路径或引用、加入对话。修复 #14758。
- Fixed: files and folders in the workbench changes list now have the same right-click menu as the file tree: reveal in the file manager, copy path or reference, add to chat. Fixes #14758.