diff --git a/apps/code/src/renderer/features/onboarding/components/ProjectSelectStep.tsx b/apps/code/src/renderer/features/onboarding/components/ProjectSelectStep.tsx index 713726247..7686f6584 100644 --- a/apps/code/src/renderer/features/onboarding/components/ProjectSelectStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/ProjectSelectStep.tsx @@ -7,8 +7,10 @@ import { useAuthStateValue, useCurrentUser, } from "@features/auth/hooks/authQueries"; -import { Command } from "@features/command/components/Command"; -import { useProjects } from "@features/projects/hooks/useProjects"; +import { + type ProjectInfo, + useProjects, +} from "@features/projects/hooks/useProjects"; import { ArrowLeft, ArrowRight, @@ -16,24 +18,43 @@ import { Check, CheckCircle, } from "@phosphor-icons/react"; -import { Box, Button, Flex, Popover, Spinner, Text } from "@radix-ui/themes"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxTrigger, +} from "@posthog/quill"; +import { Button, Flex, Spinner, Text } from "@radix-ui/themes"; import happyHog from "@renderer/assets/images/hedgehogs/happy-hog.png"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { logger } from "@utils/logger"; import { AnimatePresence, motion } from "framer-motion"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { OnboardingHogTip } from "./OnboardingHogTip"; import { StepActions } from "./StepActions"; -import "./ProjectSelect.css"; - const log = logger.scope("project-select-step"); +interface Org { + id: string; + name: string; + slug: string; +} + interface ProjectSelectStepProps { onNext: () => void; onBack: () => void; } +const TRIGGER_CLASS = + "box-border flex w-full cursor-pointer appearance-none items-center justify-between gap-3 rounded-[10px] border border-(--gray-a3) bg-(--color-panel-solid) px-[14px] py-[10px] font-[inherit] text-sm shadow-[0_1px_3px_rgba(0,0,0,0.04),0_1px_2px_rgba(0,0,0,0.02)]"; + +const CONTENT_CLASS = + "w-(--anchor-width) max-w-(--anchor-width) min-w-(--anchor-width) p-0"; + export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) { const authFetched = useAuthStateFetched(); const isAuthenticated = @@ -44,18 +65,16 @@ export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) { const [projectOpen, setProjectOpen] = useState(false); const [orgOpen, setOrgOpen] = useState(false); const [isSwitchingOrg, setIsSwitchingOrg] = useState(false); + const orgAnchorRef = useRef(null); + const projectAnchorRef = useRef(null); const client = useOptionalAuthenticatedClient(); const queryClient = useQueryClient(); const { data: fullUser } = useCurrentUser({ client }); - const organizations = useMemo(() => { + const organizations = useMemo(() => { if (!fullUser?.organizations) return []; - return fullUser.organizations as Array<{ - id: string; - name: string; - slug: string; - }>; + return fullUser.organizations as Org[]; }, [fullUser]); const currentOrg = fullUser?.organization as @@ -63,6 +82,24 @@ export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) { | undefined; const hasMultipleOrgs = organizations.length > 1; + const sortedOrgs = useMemo( + () => [...organizations].sort((a, b) => a.name.localeCompare(b.name)), + [organizations], + ); + const sortedProjects = useMemo( + () => [...projects].sort((a, b) => a.name.localeCompare(b.name)), + [projects], + ); + + const selectedOrg = useMemo( + () => sortedOrgs.find((o) => o.id === currentOrg?.id) ?? null, + [sortedOrgs, currentOrg?.id], + ); + const selectedProject = useMemo( + () => sortedProjects.find((p) => p.id === currentProjectId) ?? null, + [sortedProjects, currentProjectId], + ); + const switchOrgMutation = useMutation({ mutationFn: async (orgId: string) => { if (!client) return; @@ -163,76 +200,75 @@ export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) { Organization - - - - - + items={sortedOrgs} + value={selectedOrg} + onValueChange={(value) => { + const org = value as Org | null; + if (org && org.id !== currentOrg?.id) { + switchOrgMutation.mutate(org.id); + } + setOrgOpen(false); + }} + open={orgOpen} + onOpenChange={setOrgOpen} + itemToStringLabel={(org) => org.name} + itemToStringValue={(org) => org.id} + > + + + {currentOrg?.name ?? "Select organization..."} + + + + } + /> + - - - - - No organizations found. - - {[...organizations] - .sort((a, b) => a.name.localeCompare(b.name)) - .map((org) => ( - { - if (org.id !== currentOrg?.id) { - switchOrgMutation.mutate(org.id); - } - setOrgOpen(false); - }} - > - - - - {org.name} - - - {org.id === currentOrg?.id && ( - - )} - - - ))} - - - - + + No organizations found. + + {(org: Org) => ( + + + {org.name} + {org.id === currentOrg?.id && ( + + )} + + + )} + + + )} @@ -249,85 +285,87 @@ export function ProjectSelectStep({ onNext, onBack }: ProjectSelectStepProps) { Project - + items={sortedProjects} + value={selectedProject} + onValueChange={(value) => { + const project = value as ProjectInfo | null; + if (project) { + selectProjectMutation.mutate(project.id); + } + setProjectOpen(false); + }} open={projectOpen} onOpenChange={setProjectOpen} + itemToStringLabel={(project) => project.name} + itemToStringValue={(project) => String(project.id)} > - - - - + {currentProject.organization.name} + + )} + + + + } + /> + - - - - No projects found. - {[...projects] - .sort((a, b) => a.name.localeCompare(b.name)) - .map((project) => ( - { - selectProjectMutation.mutate(project.id); - setProjectOpen(false); - }} - > - - - - {project.name} - - - {project.id === currentProjectId && ( - - )} - - - ))} - - - - + + No projects found. + + {(project: ProjectInfo) => ( + + + {project.name} + {project.id === currentProjectId && ( + + )} + + + )} + + + )} diff --git a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx index 01c73dc4d..0911dd08a 100644 --- a/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx +++ b/apps/code/src/renderer/features/settings/components/SettingsDialog.tsx @@ -53,7 +53,7 @@ interface SidebarItem { const SIDEBAR_ITEMS: SidebarItem[] = [ { id: "general", label: "General", icon: }, - { id: "plan-usage", label: "Plan & Usage", icon: }, + { id: "plan-usage", label: "Plan & usage", icon: }, { id: "workspaces", label: "Workspaces", icon: }, { id: "worktrees", label: "Worktrees", icon: }, { @@ -85,7 +85,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [ const CATEGORY_TITLES: Record = { general: "General", - "plan-usage": "Plan & Usage", + "plan-usage": "Plan & usage", workspaces: "Workspaces", worktrees: "Worktrees", environments: "Environments",