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,52 +1,27 @@
import { CommandDialog, CommandInput, CommandList } from "@superset/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@superset/ui/dialog";
import { toast } from "@superset/ui/sonner";
import { Tabs, TabsList, TabsTrigger } from "@superset/ui/tabs";
import { useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useOpenProject } from "renderer/react-query/projects";
import {
useCloseNewWorkspaceModal,
useNewWorkspaceModalOpen,
usePreSelectedProjectId,
} from "renderer/stores/new-workspace-modal";
import { BranchesGroup } from "./components/BranchesGroup";
import { IssuesGroup } from "./components/IssuesGroup";
import { ProjectSelector } from "./components/ProjectSelector";
import { PromptGroup } from "./components/PromptGroup";
import { PullRequestsGroup } from "./components/PullRequestsGroup";

type Tab = "prompt" | "issues" | "pull-requests" | "branches";
import { NewWorkspaceModalContent } from "./components/NewWorkspaceModalContent";
import { NewWorkspaceModalDraftProvider } from "./NewWorkspaceModalDraftContext";

export function NewWorkspaceModal() {
const isOpen = useNewWorkspaceModalOpen();
const closeModal = useCloseNewWorkspaceModal();
const preSelectedProjectId = usePreSelectedProjectId();
const [activeTab, setActiveTab] = useState<Tab>("prompt");
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
null,
);
const navigate = useNavigate();
const { openNew } = useOpenProject();

const { data: recentProjects = [] } =
electronTrpc.projects.getRecents.useQuery();

// Sync pre-selected project when modal opens
// biome-ignore lint/correctness/useExhaustiveDependencies: reset on modal open
useEffect(() => {
if (!isOpen) return;
if (preSelectedProjectId) {
setSelectedProjectId(preSelectedProjectId);
} else if (recentProjects.length > 0 && !selectedProjectId) {
setSelectedProjectId(recentProjects[0].id);
}
}, [isOpen]);

const selectedProject = recentProjects.find(
(p) => p.id === selectedProjectId,
);
const isListTab = activeTab !== "prompt";
const preSelectedProjectId = usePreSelectedProjectId();

const handleImportRepo = async () => {
closeModal();
Expand All @@ -66,67 +41,26 @@ export function NewWorkspaceModal() {
};

return (
<CommandDialog
open={isOpen}
onOpenChange={(open) => !open && closeModal()}
title="New Workspace"
description="Create a new workspace from a PR, branch, issue, or prompt."
showCloseButton={false}
className="sm:max-w-[560px] max-h-[min(70vh,600px)] !top-[calc(50%-min(35vh,300px))] !-translate-y-0 flex flex-col"
>
<div className="flex items-center justify-between border-b px-3 py-2">
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as Tab)}
<NewWorkspaceModalDraftProvider onClose={closeModal}>
<Dialog open={isOpen} onOpenChange={(open) => !open && closeModal()}>
<DialogHeader className="sr-only">
<DialogTitle>New Workspace</DialogTitle>
<DialogDescription>
Create a new workspace from a PR, branch, issue, or prompt.
</DialogDescription>
</DialogHeader>
<DialogContent
showCloseButton={false}
className="sm:max-w-[560px] max-h-[min(70vh,600px)] !top-[calc(50%-min(35vh,300px))] !-translate-y-0 flex flex-col overflow-hidden p-0"
>
<TabsList>
<TabsTrigger value="prompt">Prompt</TabsTrigger>
<TabsTrigger value="issues">Issues</TabsTrigger>
<TabsTrigger value="pull-requests">Pull requests</TabsTrigger>
<TabsTrigger value="branches">Branches</TabsTrigger>
</TabsList>
</Tabs>
<ProjectSelector
selectedProjectId={selectedProjectId}
selectedProjectName={selectedProject?.name ?? null}
recentProjects={recentProjects.filter((p) => Boolean(p.id))}
onSelectProject={setSelectedProjectId}
onImportRepo={handleImportRepo}
onNewProject={handleNewProject}
/>
</div>

{isListTab && (
<CommandInput
placeholder={
activeTab === "issues"
? "Search by slug, title, or description"
: activeTab === "branches"
? "Search by name"
: "Search by title, number, or author"
}
/>
)}

<CommandList className="!max-h-none flex-1 overflow-y-auto">
{activeTab === "pull-requests" && (
<PullRequestsGroup
projectId={selectedProjectId}
githubOwner={selectedProject?.githubOwner ?? null}
repoName={selectedProject?.name ?? null}
onClose={closeModal}
<NewWorkspaceModalContent
isOpen={isOpen}
preSelectedProjectId={preSelectedProjectId}
onImportRepo={handleImportRepo}
onNewProject={handleNewProject}
/>
)}
{activeTab === "branches" && (
<BranchesGroup projectId={selectedProjectId} onClose={closeModal} />
)}
{activeTab === "issues" && (
<IssuesGroup projectId={selectedProjectId} onClose={closeModal} />
)}
{activeTab === "prompt" && (
<PromptGroup projectId={selectedProjectId} onClose={closeModal} />
)}
</CommandList>
</CommandDialog>
</DialogContent>
</Dialog>
</NewWorkspaceModalDraftProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { toast } from "@superset/ui/sonner";
import {
createContext,
type PropsWithChildren,
useCallback,
useContext,
useMemo,
useState,
} from "react";

export type NewWorkspaceModalTab =
| "prompt"
| "issues"
| "pull-requests"
| "branches";

export interface NewWorkspaceModalDraft {
activeTab: NewWorkspaceModalTab;
selectedProjectId: string | null;
prompt: string;
branchName: string;
branchNameEdited: boolean;
baseBranch: string | null;
showAdvanced: boolean;
runSetupScript: boolean;
branchSearch: string;
issuesQuery: string;
pullRequestsQuery: string;
branchesQuery: string;
}

interface NewWorkspaceModalDraftState extends NewWorkspaceModalDraft {
draftVersion: number;
}

const initialDraft: NewWorkspaceModalDraft = {
activeTab: "prompt",
selectedProjectId: null,
prompt: "",
branchName: "",
branchNameEdited: false,
baseBranch: null,
showAdvanced: false,
runSetupScript: true,
branchSearch: "",
issuesQuery: "",
pullRequestsQuery: "",
branchesQuery: "",
};

function buildInitialDraftState(): NewWorkspaceModalDraftState {
return {
...initialDraft,
draftVersion: 0,
};
}

interface NewWorkspaceModalActionMessages {
loading: string;
success: string;
error: (err: unknown) => string;
}

interface NewWorkspaceModalDraftContextValue {
draft: NewWorkspaceModalDraft;
draftVersion: number;
closeModal: () => void;
closeAndResetDraft: () => void;
runAsyncAction: <T>(
promise: Promise<T>,
messages: NewWorkspaceModalActionMessages,
) => Promise<T>;
updateDraft: (patch: Partial<NewWorkspaceModalDraft>) => void;
resetDraft: () => void;
resetDraftIfVersion: (draftVersion: number) => void;
}

const NewWorkspaceModalDraftContext =
createContext<NewWorkspaceModalDraftContextValue | null>(null);

export function NewWorkspaceModalDraftProvider({
children,
onClose,
}: PropsWithChildren<{ onClose: () => void }>) {
const [state, setState] = useState(buildInitialDraftState);

const updateDraft = useCallback((patch: Partial<NewWorkspaceModalDraft>) => {
setState((state) => ({
...state,
...patch,
draftVersion: state.draftVersion + 1,
}));
}, []);

const resetDraft = useCallback(() => {
setState((state) => ({
...initialDraft,
draftVersion: state.draftVersion + 1,
}));
}, []);

const resetDraftIfVersion = useCallback((draftVersion: number) => {
setState((state) =>
state.draftVersion !== draftVersion
? state
: {
...initialDraft,
draftVersion: state.draftVersion + 1,
},
);
}, []);

const closeAndResetDraft = useCallback(() => {
resetDraft();
onClose();
}, [onClose, resetDraft]);

const runAsyncAction = useCallback(
<T,>(promise: Promise<T>, messages: NewWorkspaceModalActionMessages) => {
const submitDraftVersion = state.draftVersion;
onClose();
toast.promise(promise, {
loading: messages.loading,
success: messages.success,
error: (err) => messages.error(err),
});
void promise
.then(() => {
resetDraftIfVersion(submitDraftVersion);
})
.catch(() => undefined);
return promise;
},
[onClose, resetDraftIfVersion, state.draftVersion],
);

const value = useMemo<NewWorkspaceModalDraftContextValue>(
() => ({
draft: {
activeTab: state.activeTab,
selectedProjectId: state.selectedProjectId,
prompt: state.prompt,
branchName: state.branchName,
branchNameEdited: state.branchNameEdited,
baseBranch: state.baseBranch,
showAdvanced: state.showAdvanced,
runSetupScript: state.runSetupScript,
branchSearch: state.branchSearch,
issuesQuery: state.issuesQuery,
pullRequestsQuery: state.pullRequestsQuery,
branchesQuery: state.branchesQuery,
},
draftVersion: state.draftVersion,
closeModal: onClose,
closeAndResetDraft,
runAsyncAction,
updateDraft,
resetDraft,
resetDraftIfVersion,
}),
[
closeAndResetDraft,
onClose,
resetDraft,
resetDraftIfVersion,
runAsyncAction,
state,
updateDraft,
],
);

return (
<NewWorkspaceModalDraftContext.Provider value={value}>
{children}
</NewWorkspaceModalDraftContext.Provider>
);
}

export function useNewWorkspaceModalDraft() {
const context = useContext(NewWorkspaceModalDraftContext);
if (!context) {
throw new Error(
"useNewWorkspaceModalDraft must be used within NewWorkspaceModalDraftProvider",
);
}
return context;
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { Button } from "@superset/ui/button";
import { CommandEmpty, CommandGroup, CommandItem } from "@superset/ui/command";
import { toast } from "@superset/ui/sonner";
import { useNavigate } from "@tanstack/react-router";
import { useCallback, useMemo } from "react";
import { GoArrowUpRight, GoGitBranch, GoGlobe } from "react-icons/go";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useCreateBranchWorkspace } from "renderer/react-query/workspaces";
import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation";
import { useHotkeysStore } from "renderer/stores/hotkeys/store";
import { useNewWorkspaceModalDraft } from "../../NewWorkspaceModalDraftContext";

interface BranchesGroupProps {
projectId: string | null;
onClose: () => void;
}

export function BranchesGroup({ projectId, onClose }: BranchesGroupProps) {
export function BranchesGroup({ projectId }: BranchesGroupProps) {
const platform = useHotkeysStore((state) => state.platform);
const modKey = platform === "darwin" ? "⌘" : "Ctrl";
const navigate = useNavigate();
const createBranchWorkspace = useCreateBranchWorkspace();
const { closeAndResetDraft, runAsyncAction } = useNewWorkspaceModalDraft();

// Fast query: local branches + cached remote refs (no network)
const { data: localData, isLoading: isLocalLoading } =
Expand Down Expand Up @@ -63,8 +63,7 @@ export function BranchesGroup({ projectId, onClose }: BranchesGroupProps) {
const handleCreate = useCallback(
(branchName: string) => {
if (!projectId) return;
onClose();
toast.promise(
void runAsyncAction(
createBranchWorkspace.mutateAsync({
projectId,
branch: branchName,
Expand All @@ -77,15 +76,15 @@ export function BranchesGroup({ projectId, onClose }: BranchesGroupProps) {
},
);
},
[projectId, onClose, createBranchWorkspace],
[createBranchWorkspace, projectId, runAsyncAction],
);

const handleOpen = useCallback(
(workspaceId: string) => {
onClose();
closeAndResetDraft();
navigateToWorkspace(workspaceId, navigate);
},
[onClose, navigate],
[closeAndResetDraft, navigate],
);

if (!projectId) {
Expand Down
Loading
Loading