mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): make missing project checkouts actionable (#2159)
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__ ?? [],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user