Skip to content
Closed
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
Expand Up @@ -11,7 +11,7 @@ import { CategorySection } from "./components/CategorySection";
import { ChangesHeader } from "./components/ChangesHeader";
import { CommitInput } from "./components/CommitInput";
import { CommitItem } from "./components/CommitItem";
import { FileList } from "./components/FileList";
import { type FileContextMenuProps, FileList } from "./components/FileList";

interface ChangesViewProps {
/** Single click - opens in preview mode */
Expand All @@ -26,11 +26,14 @@ interface ChangesViewProps {
category: ChangeCategory,
commitHash?: string,
) => void;
/** Context menu props - if provided, enables right-click menu (without discard - that's added per category) */
contextMenuProps?: Omit<FileContextMenuProps, "onDiscardChanges">;
}

export function ChangesView({
onFileOpen,
onFileOpenPinned,
contextMenuProps,
}: ChangesViewProps) {
const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery();
const worktreePath = activeWorkspace?.worktreePath;
Expand Down Expand Up @@ -102,6 +105,25 @@ export function ChangesView({
},
});

const discardChangesMutation = trpc.changes.discardChanges.useMutation({
onSuccess: () => refetch(),
onError: (error, variables) => {
console.error(
`Failed to discard changes for ${variables.filePath}:`,
error,
);
toast.error(`Failed to discard changes: ${error.message}`);
},
});

const deleteUntrackedMutation = trpc.changes.deleteUntracked.useMutation({
onSuccess: () => refetch(),
onError: (error, variables) => {
console.error(`Failed to delete ${variables.filePath}:`, error);
toast.error(`Failed to delete file: ${error.message}`);
},
});

const {
expandedSections,
fileListViewMode,
Expand Down Expand Up @@ -187,6 +209,34 @@ export function ChangesView({
});
};

// Handle discard changes for unstaged files
const handleDiscardChanges = (file: ChangedFile) => {
if (!worktreePath) return;
// Untracked files need to be deleted, modified files use git checkout
if (file.status === "untracked") {
deleteUntrackedMutation.mutate({
worktreePath,
filePath: file.path,
});
} else {
discardChangesMutation.mutate({
worktreePath,
filePath: file.path,
});
}
};
Comment on lines +212 to +227
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.

⚠️ Potential issue | 🟡 Minor

Consider adding a confirmation dialog for destructive discard operations.

handleDiscardChanges can permanently delete untracked files or discard uncommitted changes. These are irreversible operations that users might trigger accidentally via the context menu. Consider adding a confirmation dialog, especially for the deleteUntrackedMutation path.

🤖 Prompt for AI Agents
In
@apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx
around lines 212 - 227, handleDiscardChanges currently performs irreversible
actions without confirmation; before invoking deleteUntrackedMutation or
discardChangesMutation, show a confirmation dialog/modal (use the app's existing
confirmation UI or a simple window.confirm) that clearly states the file path
and whether it will be deleted (for untracked) or have changes discarded (for
modified), and only proceed to call deleteUntrackedMutation.mutate or
discardChangesMutation.mutate if the user confirms; for clarity, use distinct
messages for the untracked (delete) and tracked (discard) branches and abort on
cancel.


// Create context menu props, optionally including discard for unstaged files
const getContextMenuProps = (
includeDiscard = false,
): FileContextMenuProps | undefined => {
if (!contextMenuProps) return undefined;
return {
...contextMenuProps,
onDiscardChanges: includeDiscard ? handleDiscardChanges : undefined,
};
};

if (!worktreePath) {
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground text-sm p-4">
Expand Down Expand Up @@ -279,6 +329,7 @@ export function ChangesView({
onFileDoubleClick={(file) =>
handleFileDoubleClick(file, "against-base")
}
contextMenuProps={getContextMenuProps()}
/>
</CategorySection>

Expand All @@ -300,6 +351,7 @@ export function ChangesView({
onFileSelect={handleCommitFileSelect}
onFileDoubleClick={handleCommitFileDoubleClick}
viewMode={fileListViewMode}
contextMenuProps={getContextMenuProps()}
/>
))}
</CategorySection>
Expand Down Expand Up @@ -347,6 +399,7 @@ export function ChangesView({
})
}
isActioning={unstageFileMutation.isPending}
contextMenuProps={getContextMenuProps()}
/>
</CategorySection>

Expand Down Expand Up @@ -393,6 +446,7 @@ export function ChangesView({
})
}
isActioning={stageFileMutation.isPending}
contextMenuProps={getContextMenuProps(true)}
/>
</CategorySection>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { cn } from "@superset/ui/utils";
import { HiChevronRight } from "react-icons/hi2";
import type { ChangedFile, CommitInfo } from "shared/changes-types";
import type { ChangesViewMode } from "../../types";
import { FileList } from "../FileList";
import { type FileContextMenuProps, FileList } from "../FileList";

interface CommitItemProps {
commit: CommitInfo;
Expand All @@ -20,6 +20,8 @@ interface CommitItemProps {
/** Double click - opens pinned (permanent) */
onFileDoubleClick?: (file: ChangedFile, commitHash: string) => void;
viewMode: ChangesViewMode;
/** Context menu props - if provided, enables right-click menu */
contextMenuProps?: FileContextMenuProps;
}

function formatRelativeDate(date: Date): string {
Expand All @@ -45,6 +47,7 @@ export function CommitItem({
onFileSelect,
onFileDoubleClick,
viewMode,
contextMenuProps,
}: CommitItemProps) {
const hasFiles = commit.files.length > 0;

Expand Down Expand Up @@ -94,6 +97,7 @@ export function CommitItem({
onFileSelect={handleFileSelect}
onFileDoubleClick={handleFileDoubleClick}
showStats={false}
contextMenuProps={contextMenuProps}
/>
</CollapsibleContent>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import { Button } from "@superset/ui/button";
import { ContextMenuTrigger } from "@superset/ui/context-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { cn } from "@superset/ui/utils";
import { HiMiniMinus, HiMiniPlus } from "react-icons/hi2";
import type { Tab } from "renderer/stores/tabs/types";
import type { ChangedFile } from "shared/changes-types";
import { getStatusColor, getStatusIndicator } from "../../utils";
import {
FileItemContextMenu,
type FileItemContextMenuActions,
} from "./FileItemContextMenu";

export interface FileItemContextMenuProps {
currentTabId: string;
availableTabs: Tab[];
onOpenInSplitHorizontal: () => void;
onOpenInSplitVertical: () => void;
onOpenInApp: () => void;
onOpenInNewTab: () => void;
onMoveToTab: (tabId: string) => void;
onDiscardChanges?: () => void;
}

interface FileItemProps {
file: ChangedFile;
Expand All @@ -21,6 +38,8 @@ interface FileItemProps {
onUnstage?: () => void;
/** Whether the action is currently pending */
isActioning?: boolean;
/** Context menu props - if provided, enables right-click menu */
contextMenuProps?: FileItemContextMenuProps;
}

function LevelIndicators({ level }: { level: number }) {
Expand Down Expand Up @@ -50,6 +69,7 @@ export function FileItem({
onStage,
onUnstage,
isActioning = false,
contextMenuProps,
}: FileItemProps) {
const fileName = getFileName(file.path);
const statusBadgeColor = getStatusColor(file.status);
Expand All @@ -59,7 +79,7 @@ export function FileItem({
const hasIndent = level > 0;
const hasAction = onStage || onUnstage;

return (
const content = (
<div
className={cn(
"group w-full flex items-stretch gap-1 px-1.5 text-left rounded-sm",
Expand Down Expand Up @@ -152,4 +172,27 @@ export function FileItem({
)}
</div>
);

if (contextMenuProps) {
const actions: FileItemContextMenuActions = {
onOpenInSplitHorizontal: contextMenuProps.onOpenInSplitHorizontal,
onOpenInSplitVertical: contextMenuProps.onOpenInSplitVertical,
onOpenInApp: contextMenuProps.onOpenInApp,
onOpenInNewTab: contextMenuProps.onOpenInNewTab,
onMoveToTab: contextMenuProps.onMoveToTab,
onDiscardChanges: contextMenuProps.onDiscardChanges,
};

return (
<FileItemContextMenu
actions={actions}
currentTabId={contextMenuProps.currentTabId}
availableTabs={contextMenuProps.availableTabs}
>
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
</FileItemContextMenu>
);
}

return content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
} from "@superset/ui/context-menu";
import type { ReactNode } from "react";
import {
LuAppWindow,
LuColumns2,
LuMoveRight,
LuPlus,
LuRows2,
LuTrash2,
} from "react-icons/lu";
import type { Tab } from "renderer/stores/tabs/types";

export interface FileItemContextMenuActions {
onOpenInSplitHorizontal: () => void;
onOpenInSplitVertical: () => void;
onOpenInApp: () => void;
onOpenInNewTab: () => void;
onMoveToTab: (tabId: string) => void;
onDiscardChanges?: () => void;
}

interface FileItemContextMenuProps {
children: ReactNode;
actions: FileItemContextMenuActions;
currentTabId: string;
availableTabs: Tab[];
}

export function FileItemContextMenu({
children,
actions,
currentTabId,
availableTabs,
}: FileItemContextMenuProps) {
const targetTabs = availableTabs.filter((t) => t.id !== currentTabId);

return (
<ContextMenu>
<ContextMenuContent>
{/* Open actions */}
<ContextMenuItem onSelect={actions.onOpenInSplitHorizontal}>
<LuRows2 className="size-4" />
Open in Split Pane (Horizontal)
</ContextMenuItem>
<ContextMenuItem onSelect={actions.onOpenInSplitVertical}>
<LuColumns2 className="size-4" />
Open in Split Pane (Vertical)
</ContextMenuItem>

<ContextMenuSeparator />

<ContextMenuItem onSelect={actions.onOpenInApp}>
<LuAppWindow className="size-4" />
Open in App
</ContextMenuItem>

<ContextMenuSeparator />

{/* Tab actions */}
<ContextMenuSub>
<ContextMenuSubTrigger className="gap-2">
<LuMoveRight className="size-4" />
Open in Tab
</ContextMenuSubTrigger>
<ContextMenuSubContent>
{targetTabs.map((tab) => (
<ContextMenuItem
key={tab.id}
onSelect={() => actions.onMoveToTab(tab.id)}
>
{tab.userTitle || tab.name}
</ContextMenuItem>
))}
{targetTabs.length > 0 && <ContextMenuSeparator />}
<ContextMenuItem onSelect={actions.onOpenInNewTab}>
<LuPlus className="size-4" />
New Tab
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>

{/* Destructive actions */}
{actions.onDiscardChanges && (
<>
<ContextMenuSeparator />
<ContextMenuItem
variant="destructive"
onSelect={actions.onDiscardChanges}
>
<LuTrash2 className="size-4" />
Discard Changes
</ContextMenuItem>
</>
)}
</ContextMenuContent>
{children}
</ContextMenu>
);
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { FileItem } from "./FileItem";
export { FileItem, type FileItemContextMenuProps } from "./FileItem";
export { FileItemContextMenu } from "./FileItemContextMenu";
Loading