Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useDiffStats } from "renderer/hooks/host-service/useDiffStats";
import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions";
import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider";
import { RenameBranchDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components";
import { useV2WorkspaceNotificationStatus } from "renderer/stores/v2-notifications";
import type { DashboardSidebarWorkspace } from "../../types";
import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog";
Expand Down Expand Up @@ -67,6 +70,13 @@ export function DashboardSidebarWorkspaceItem({
});

const navigate = useNavigate();
const { v2Workspaces: v2WorkspaceActions } = useOptimisticCollectionActions();
const [renameBranchTarget, setRenameBranchTarget] = useState<string | null>(
null,
);
const handleAfterBranchRename = (newBranchName: string) => {
v2WorkspaceActions.updateWorkspace(id, { branch: newBranchName });
};
const isPending = !!creationStatus;
// Keep the delete dialog outside the hidden wrapper below — the destroy
// flow reopens it into an error pane on conflict/teardown-failed.
Expand Down Expand Up @@ -122,6 +132,7 @@ export function DashboardSidebarWorkspaceItem({
<DashboardSidebarWorkspaceHoverCardContent
workspace={workspace}
diffStats={diffStats}
onEditBranchClick={setRenameBranchTarget}
/>
}
isLocalWorkspace={hostType === "local-device"}
Expand Down Expand Up @@ -153,6 +164,17 @@ export function DashboardSidebarWorkspaceItem({
onDeleted={handleDeleted}
/>
)}
{renameBranchTarget && (
<RenameBranchDialog
workspaceId={id}
currentBranchName={renameBranchTarget}
open={renameBranchTarget !== null}
onOpenChange={(open) => {
if (!open) setRenameBranchTarget(null);
}}
onAfterRename={handleAfterBranchRename}
/>
)}
Comment on lines +167 to +177
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant open prop inside conditional render

renameBranchTarget !== null is always true inside the {renameBranchTarget && ...} guard, so open is a constant true here. The same pattern appears in the second return path (line ~253) and in CollapsedWorkspaceItem.tsx / WorkspaceContextMenu.tsx. Consider either always rendering the dialog and driving visibility purely through open={!!renameBranchTarget}, or simplifying to open={true} inside the guard.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
Line: 167-177

Comment:
**Redundant `open` prop inside conditional render**

`renameBranchTarget !== null` is always `true` inside the `{renameBranchTarget && ...}` guard, so `open` is a constant `true` here. The same pattern appears in the second return path (line ~253) and in `CollapsedWorkspaceItem.tsx` / `WorkspaceContextMenu.tsx`. Consider either always rendering the dialog and driving visibility purely through `open={!!renameBranchTarget}`, or simplifying to `open={true}` inside the guard.

How can I resolve this? If you propose a fix, please make it concise.

</>
);
}
Expand Down Expand Up @@ -196,6 +218,7 @@ export function DashboardSidebarWorkspaceItem({
<DashboardSidebarWorkspaceHoverCardContent
workspace={workspace}
diffStats={diffStats}
onEditBranchClick={setRenameBranchTarget}
/>
}
onCreateSection={handleCreateSection}
Expand Down Expand Up @@ -227,6 +250,17 @@ export function DashboardSidebarWorkspaceItem({
onDeleted={handleDeleted}
/>
)}
{renameBranchTarget && (
<RenameBranchDialog
workspaceId={id}
currentBranchName={renameBranchTarget}
open={renameBranchTarget !== null}
onOpenChange={(open) => {
if (!open) setRenameBranchTarget(null);
}}
onAfterRename={handleAfterBranchRename}
/>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { Button } from "@superset/ui/button";
import { Kbd, KbdGroup } from "@superset/ui/kbd";
import { formatDistanceToNow } from "date-fns";
import { FaGithub } from "react-icons/fa";
import { LuExternalLink, LuGlobe, LuTriangleAlert } from "react-icons/lu";
import {
LuExternalLink,
LuGlobe,
LuPencil,
LuTriangleAlert,
} from "react-icons/lu";
import type { DiffStats } from "renderer/hooks/host-service/useDiffStats";
import { useHotkeyDisplay } from "renderer/hotkeys";
import type { DashboardSidebarWorkspace } from "../../../../types";
Expand All @@ -14,11 +19,13 @@ import { ReviewStatus } from "./components/ReviewStatus";
interface DashboardSidebarWorkspaceHoverCardContentProps {
workspace: DashboardSidebarWorkspace;
diffStats: DiffStats | null;
onEditBranchClick?: (branchName: string) => void;
}

export function DashboardSidebarWorkspaceHoverCardContent({
workspace,
diffStats,
onEditBranchClick,
}: DashboardSidebarWorkspaceHoverCardContentProps) {
const {
name,
Expand Down Expand Up @@ -59,23 +66,37 @@ export function DashboardSidebarWorkspaceHoverCardContent({
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">
Branch
</span>
{repoUrl && branchExistsOnRemote ? (
<a
href={`${repoUrl}/tree/${branch}`}
target="_blank"
rel="noopener noreferrer"
className={`flex items-center gap-1 font-mono break-all hover:underline ${hasCustomAlias ? "text-xs" : "text-sm"}`}
>
{branch}
<LuExternalLink className="size-3 shrink-0" />
</a>
) : (
<code
className={`font-mono break-all block ${hasCustomAlias ? "text-xs" : "text-sm"}`}
>
{branch}
</code>
)}
<div className="flex items-center gap-1.5">
{onEditBranchClick ? (
<button
type="button"
onClick={() => onEditBranchClick(branch)}
className={`group/branch flex min-w-0 flex-1 items-center gap-1 font-mono break-all text-left hover:text-foreground hover:underline ${hasCustomAlias ? "text-xs" : "text-sm"}`}
title="Rename branch"
>
<span className="break-all">{branch}</span>
<LuPencil className="size-3 shrink-0 opacity-0 group-hover/branch:opacity-100 transition-opacity" />
</button>
) : (
<code
className={`font-mono break-all block min-w-0 flex-1 ${hasCustomAlias ? "text-xs" : "text-sm"}`}
>
{branch}
</code>
)}
{repoUrl && branchExistsOnRemote && (
<a
href={`${repoUrl}/tree/${branch}`}
target="_blank"
rel="noopener noreferrer"
className="shrink-0 text-muted-foreground hover:text-foreground"
title="Open branch on GitHub"
onClick={(e) => e.stopPropagation()}
>
<LuExternalLink className="size-3" />
</a>
)}
</div>
</div>
<span className="text-xs text-muted-foreground block">
{formatDistanceToNow(createdAt, { addSuffix: true })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import { LuCopy, LuGitBranch, LuX } from "react-icons/lu";
import { createContextMenuDeleteDialogCoordinator } from "renderer/react-query/workspaces/useWorkspaceDeleteHandler";
import type { ActivePaneStatus } from "shared/tabs-types";
import { STROKE_WIDTH } from "../constants";
import { DeleteWorkspaceDialog, WorkspaceHoverCardContent } from "./components";
import {
DeleteWorkspaceDialog,
RenameBranchDialog,
WorkspaceHoverCardContent,
} from "./components";
import { HOVER_CARD_CLOSE_DELAY, HOVER_CARD_OPEN_DELAY } from "./constants";
import { WorkspaceIcon } from "./WorkspaceIcon";

Expand Down Expand Up @@ -62,6 +66,9 @@ export function CollapsedWorkspaceItem({
[onDeleteClick],
);
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const [renameBranchTarget, setRenameBranchTarget] = useState<string | null>(
null,
);

const collapsedButton = (
<button
Expand Down Expand Up @@ -160,7 +167,11 @@ export function CollapsedWorkspaceItem({
</ContextMenuContent>
</ContextMenu>
<HoverCardContent side="right" align="start" className="w-72">
<WorkspaceHoverCardContent workspaceId={id} workspaceAlias={name} />
<WorkspaceHoverCardContent
workspaceId={id}
workspaceAlias={name}
onEditBranchClick={setRenameBranchTarget}
/>
</HoverCardContent>
</HoverCard>
<DeleteWorkspaceDialog
Expand All @@ -170,6 +181,16 @@ export function CollapsedWorkspaceItem({
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
/>
{renameBranchTarget && (
<RenameBranchDialog
workspaceId={id}
currentBranchName={renameBranchTarget}
open={renameBranchTarget !== null}
onOpenChange={(open) => {
if (!open) setRenameBranchTarget(null);
}}
/>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
import { createContextMenuDeleteDialogCoordinator } from "renderer/react-query/workspaces/useWorkspaceDeleteHandler";
import { useWorkspaceSelectionStore } from "renderer/stores/workspace-selection";
import { STROKE_WIDTH } from "../constants";
import { WorkspaceHoverCardContent } from "./components";
import { RenameBranchDialog, WorkspaceHoverCardContent } from "./components";
import { HOVER_CARD_CLOSE_DELAY, HOVER_CARD_OPEN_DELAY } from "./constants";

interface WorkspaceContextMenuProps {
Expand Down Expand Up @@ -77,6 +77,9 @@ export function WorkspaceContextMenu({
children,
}: WorkspaceContextMenuProps) {
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const [renameBranchTarget, setRenameBranchTarget] = useState<string | null>(
null,
);
const contextMenuSelectionRef = useRef<string[]>([]);
const selectionStore = useWorkspaceSelectionStore;
const moveToSection = useMoveWorkspaceToSection();
Expand Down Expand Up @@ -246,8 +249,22 @@ export function WorkspaceContextMenu({
</ContextMenuContent>
</ContextMenu>
<HoverCardContent side="right" align="start" className="w-72">
<WorkspaceHoverCardContent workspaceId={id} workspaceAlias={name} />
<WorkspaceHoverCardContent
workspaceId={id}
workspaceAlias={name}
onEditBranchClick={setRenameBranchTarget}
/>
</HoverCardContent>
{renameBranchTarget && (
<RenameBranchDialog
workspaceId={id}
currentBranchName={renameBranchTarget}
open={renameBranchTarget !== null}
onOpenChange={(open) => {
if (!open) setRenameBranchTarget(null);
}}
/>
)}
</HoverCard>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Button } from "@superset/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@superset/ui/dialog";
import { Input } from "@superset/ui/input";
import { Label } from "@superset/ui/label";
import { toast } from "@superset/ui/sonner";
import { useEffect, useState } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { getHostServiceClientByUrl } from "renderer/lib/host-service-client";
import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider";

interface RenameBranchDialogProps {
workspaceId: string;
currentBranchName: string;
open: boolean;
onOpenChange: (open: boolean) => void;
onAfterRename?: (newName: string) => void;
}

export function RenameBranchDialog({
workspaceId,
currentBranchName,
open,
onOpenChange,
onAfterRename,
}: RenameBranchDialogProps) {
const [value, setValue] = useState(currentBranchName);
const [isSubmitting, setIsSubmitting] = useState(false);
const electronUtils = electronTrpc.useUtils();
const { activeHostUrl } = useLocalHostService();

useEffect(() => {
if (open) setValue(currentBranchName);
}, [open, currentBranchName]);

const trimmed = value.trim();
const isUnchanged = trimmed === currentBranchName;
const isInvalid = trimmed.length === 0 || isUnchanged;

const handleSubmit = async () => {
if (isInvalid || isSubmitting) return;
if (!activeHostUrl) {
toast.error("Host service is not available");
return;
}

const client = getHostServiceClientByUrl(activeHostUrl);
const renamePromise = client.git.renameBranch.mutate({
workspaceId,
oldName: currentBranchName,
newName: trimmed,
});

toast.promise(renamePromise, {
loading: `Renaming branch to ${trimmed}...`,
success: `Branch renamed to ${trimmed}`,
error: (err) =>
err instanceof Error ? err.message : "Failed to rename branch",
});

setIsSubmitting(true);
try {
Comment on lines +46 to +68
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 setIsSubmitting called after async operation begins

setIsSubmitting(true) is set after toast.promise(renamePromise, ...) starts and the RPC call is already in-flight. Because isSubmitting is React state (not a ref), if handleSubmit is invoked twice before the first re-render (e.g. Enter key + button click in the same tick), both calls pass the if (isSubmitting) return guard and two concurrent rename mutations fire. Moving setIsSubmitting(true) above the client.git.renameBranch.mutate call would close the window.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/RenameBranchDialog/RenameBranchDialog.tsx
Line: 46-68

Comment:
**`setIsSubmitting` called after async operation begins**

`setIsSubmitting(true)` is set after `toast.promise(renamePromise, ...)` starts and the RPC call is already in-flight. Because `isSubmitting` is React state (not a ref), if `handleSubmit` is invoked twice before the first re-render (e.g. Enter key + button click in the same tick), both calls pass the `if (isSubmitting) return` guard and two concurrent rename mutations fire. Moving `setIsSubmitting(true)` above the `client.git.renameBranch.mutate` call would close the window.

How can I resolve this? If you propose a fix, please make it concise.

await renamePromise;
onAfterRename?.(trimmed);
void electronUtils.workspaces.getWorktreeInfo.invalidate({
workspaceId,
});
void electronUtils.workspaces.get.invalidate({ id: workspaceId });
void electronUtils.workspaces.getAllGrouped.invalidate();
onOpenChange(false);
} catch {
// toast.promise surfaced the error to the user
} finally {
setIsSubmitting(false);
}
};

return (
<Dialog open={open} onOpenChange={onOpenChange} modal>
<DialogContent className="max-w-[420px]">
<DialogHeader>
<DialogTitle>Rename branch</DialogTitle>
<DialogDescription>
Rename the local branch. Branches that have been pushed to remote
cannot be renamed.
</DialogDescription>
</DialogHeader>
<form
onSubmit={(e) => {
e.preventDefault();
void handleSubmit();
}}
className="space-y-4"
>
<div className="space-y-1.5">
<Label htmlFor="rename-branch-input" className="text-xs">
Branch name
</Label>
<Input
id="rename-branch-input"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
void handleSubmit();
}
}}
autoFocus
disabled={isSubmitting}
spellCheck={false}
autoComplete="off"
className="font-mono"
/>
</div>
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" disabled={isInvalid || isSubmitting}>
Rename
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RenameBranchDialog } from "./RenameBranchDialog";
Loading
Loading