feat(desktop): make missing project checkouts actionable (#2159)

This commit is contained in:
thomaspblock
2026-07-20 08:15:50 +02:00
committed by GitHub
parent a6432ab133
commit ac8c526122
7 changed files with 103 additions and 48 deletions
@@ -281,7 +281,9 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
description:
error instanceof Error ? error.message : "The Git fetch failed.",
});
return;
}
toast.success("Remote state refreshed.");
}, [repoSnapshotQuery, repoStateQuery, repoSyncStatusQuery]);
// Compact branch + remote/local controls shared by the readme and Files
// tab headers.
@@ -301,6 +303,12 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
? "Local"
: "Local missing",
remoteLabel: repoSnapshotQuery.isLoading ? "Remote checking" : "Remote",
onCloneLocal: project?.cloneUrls[0]
? () => {
void handleCloneRepo();
}
: undefined,
clonePending: cloneRepoMutation.isPending,
canPush: repoSyncStatusQuery.data?.canPush ?? false,
onPush: () => {
void handlePushLocalRepo();
@@ -813,16 +821,6 @@ export function ProjectDetailScreen(props: ProjectDetailScreenProps) {
<WorkspaceTabs
key={`${project.id}:${tabsResetKey}`}
cloneAction={
!hasLocalCheckout && project.cloneUrls[0]
? {
onClone: () => {
void handleCloneRepo();
},
pending: cloneRepoMutation.isPending,
}
: undefined
}
commitDiff={commitDiffQuery.data}
commitDiffError={commitDiffQuery.error}
commitDiffLoading={commitDiffQuery.isLoading}
@@ -13,6 +13,7 @@ import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
@@ -47,12 +48,12 @@ export function RepositoryBranchDropdown({
<Button
className={
compact
? "h-7 max-w-full gap-1.5 rounded-md bg-muted px-3 font-mono text-sm font-medium hover:bg-muted/80"
: "h-6 max-w-full gap-1.5 px-2 font-mono text-sm font-semibold"
? "h-7 max-w-full gap-1.5 rounded-md px-3 font-mono text-sm font-medium hover:border-input"
: "h-6 max-w-full gap-1.5 px-2 font-mono text-sm font-semibold hover:border-input"
}
size="sm"
type="button"
variant="ghost"
variant="outline"
>
<GitBranch className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{branch}</span>
@@ -82,6 +83,9 @@ export type RepoSourceHeaderControls = {
localDisabled: boolean;
localLabel: string;
remoteLabel: string;
/** Clones the repository when no local checkout is available. */
onCloneLocal?: () => void;
clonePending?: boolean;
/** Push of local commits, available when the local checkout is ahead. */
canPush?: boolean;
onPush?: () => void;
@@ -110,15 +114,16 @@ export function RepoSourceDropdown({
controls: RepoSourceHeaderControls;
}) {
const isLocal = controls.source === "local";
const cloneLocal = controls.localDisabled && controls.onCloneLocal;
const SourceIcon = isLocal ? HardDrive : Cloud;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="h-7 max-w-full shrink-0 gap-1.5 rounded-md bg-muted px-3 text-sm font-medium hover:bg-muted/80"
className="h-7 max-w-full shrink-0 gap-1.5 rounded-md px-3 text-sm font-medium hover:border-input"
size="sm"
type="button"
variant="ghost"
variant="outline"
>
<SourceIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">
@@ -138,14 +143,33 @@ export function RepoSourceDropdown({
<Cloud className="mr-1.5 h-3.5 w-3.5 text-muted-foreground" />
{controls.remoteLabel}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
disabled={controls.localDisabled}
value="local"
>
<HardDrive className="mr-1.5 h-3.5 w-3.5 text-muted-foreground" />
{controls.localLabel}
</DropdownMenuRadioItem>
{!cloneLocal ? (
<DropdownMenuRadioItem
disabled={controls.localDisabled}
value="local"
>
<HardDrive className="mr-1.5 h-3.5 w-3.5 text-muted-foreground" />
{controls.localLabel}
</DropdownMenuRadioItem>
) : null}
</DropdownMenuRadioGroup>
{cloneLocal ? (
<DropdownMenuItem
className="group"
disabled={controls.clonePending}
onSelect={controls.onCloneLocal}
>
{controls.clonePending ? (
<Loader2 className="animate-spin text-muted-foreground" />
) : (
<DownloadCloud className="text-muted-foreground" />
)}
<span className="text-muted-foreground">{controls.localLabel}</span>
<span className="ml-auto rounded-md border border-input/60 bg-background px-2 py-0.5 text-xs font-medium text-foreground shadow-xs group-focus:border-input">
{controls.clonePending ? "Cloning…" : "Clone"}
</span>
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
);
@@ -1,6 +1,5 @@
import {
CircleDot,
Download,
GitPullRequest,
Plus,
RefreshCw,
@@ -48,11 +47,6 @@ import {
} from "./CreateIssueDialog";
import { PROJECT_PANEL_ACTION_BUTTON_CLASS } from "./projectPanelStyles";
type CloneRepositoryAction = {
onClone: () => void;
pending: boolean;
};
type CreatePullRequestAction = {
projects: Project[];
reposDir?: string | null;
@@ -106,7 +100,6 @@ function WorkItemListHeader({
}
export function WorkspaceTabs({
cloneAction,
commitDiff,
commitDiffError,
commitDiffLoading,
@@ -142,7 +135,6 @@ export function WorkspaceTabs({
terminalTitle,
viewerGitIdentity,
}: {
cloneAction?: CloneRepositoryAction;
commitDiff: ProjectRepoDiff | null | undefined;
commitDiffError: unknown;
commitDiffLoading: boolean;
@@ -276,19 +268,6 @@ export function WorkspaceTabs({
<SquareTerminal className="h-[1.125rem] w-[1.125rem]" />
</Button>
) : null}
{cloneAction ? (
<Button
className="h-8 shrink-0 gap-1.5"
disabled={cloneAction.pending}
onClick={cloneAction.onClone}
size="sm"
title="Clone repository"
variant="outline"
>
<Download className="h-4 w-4" />
{cloneAction.pending ? "Cloning…" : "Clone"}
</Button>
) : null}
{updatePullRequestAction ? (
<Button
className="h-8 shrink-0 gap-1.5"
@@ -559,7 +559,7 @@ export function ProjectsView() {
<PageHeader
className="pointer-events-auto mb-8"
description="Set up and manage your projects."
title={activeCommunity?.name || "Relay"}
title="Projects"
/>
);
+23 -2
View File
@@ -9281,12 +9281,33 @@ export function maybeInstallE2eTauriMocks() {
pulled: true,
message: "Pulled main from remote.",
};
case "clone_project_repository":
case "clone_project_repository": {
const path = "/tmp/buzz/REPOS/mock-project";
const commit = "0123456789abcdef0123456789abcdef01234567";
window.__BUZZ_E2E_PROJECT_REPO_SYNC_STATUS__ = {
local_path: path,
local_branch: "main",
local_head: commit,
local_short_head: commit.slice(0, 7),
remote_branch: "main",
remote_head: commit,
remote_short_head: commit.slice(0, 7),
merge_base: commit,
ahead_count: 0,
behind_count: 0,
has_uncommitted_changes: false,
has_untracked_files: false,
can_push: false,
push_block_reason: "Local branch is already pushed.",
can_pull: false,
pull_block_reason: "Local branch is up to date.",
};
return {
path: "/tmp/buzz/REPOS/mock-project",
path,
cloned: true,
message: "Cloned repository.",
};
}
case "sign_project_pull_request_review_request": {
const { input } = payload as {
input: {
@@ -27,6 +27,9 @@ test("top-level project lists align dates and overflow actions", async ({
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await expect(
page.getByRole("heading", { level: 1, name: "Projects" }),
).toBeVisible();
async function trailingPositions(row: import("@playwright/test").Locator) {
await waitForAnimations(page);
+32 -2
View File
@@ -327,13 +327,43 @@ test("viewer without repository ownership cannot merge", async ({ page }) => {
);
});
test("project without a checkout can be cloned", async ({ page }) => {
test("project without a checkout offers fetch feedback and dropdown cloning", async ({
page,
}) => {
await enableProjectsFeature(page);
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("button", { name: "Clone", exact: true }).click();
await expect(
page.getByRole("button", { name: "Remote", exact: true }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Remote", exact: true }),
).toHaveClass(/\bborder-input\/40\b/);
await expect(page.getByRole("button", { name: /main/ })).toHaveClass(
/\bborder-input\/40\b/,
);
await expect(
page.getByRole("button", { name: "Clone", exact: true }),
).toHaveCount(0);
await page.getByRole("button", { name: "Fetch", exact: true }).click();
await expect(page.getByText("Remote state refreshed.")).toBeVisible();
await page.getByRole("button", { name: "Remote", exact: true }).click();
const cloneItem = page.getByRole("menuitem", {
name: "Local missing Clone",
});
await expect(cloneItem.getByText("Local missing")).toHaveClass(
/text-muted-foreground/,
);
await expect(cloneItem.getByText("Clone", { exact: true })).toHaveClass(
/\bborder-input\/60\b/,
);
await cloneItem.click();
await expect(page.getByText("Cloned repository.")).toBeVisible();
await expect(
page.getByRole("button", { name: "Local", exact: true }),
).toBeVisible();
const commands = await page.evaluate(
() => window.__BUZZ_E2E_COMMANDS__ ?? [],
);