import { useState } from "react"; import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { trackEvent } from "@executor-js/react/api/analytics"; import { Button } from "@executor-js/react/components/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@executor-js/react/components/dialog"; import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from "@executor-js/react/components/dropdown-menu"; import { useAuth } from "../auth"; import { organizationsAtom } from "../auth"; import { CreateOrganizationFields, useCreateOrganizationForm } from "./create-organization-form"; // --------------------------------------------------------------------------- // Cloud-only org-switcher slot for the shared shell's account dropdown. // // The shared `Shell` renders this `orgMenuSlot` ABOVE its API-keys link. Cloud // is the only product with multiple organizations, so the switcher + create-org // dialog live here and are injected, keeping the shared shell provider-neutral. // The create-org dialog is controlled by local state so its `DialogContent` // can live outside the dropdown menu while the trigger sits inside it. // --------------------------------------------------------------------------- function CheckIcon() { return ( ); } function OrganizationSwitcherItems(props: { activeOrganizationId: string | null }) { const organizations = useAtomValue(organizationsAtom); // Switching orgs is now a pure URL navigation: the session authenticates the // user to ALL their orgs, and the slug in the path scopes every request (the // `x-executor-organization` header). No cookie to rewrite, no server switch // call — just land on the other org's URL root and the whole app re-scopes. const handleSwitch = (organization: { id: string; slug: string }) => { if (organization.id === props.activeOrganizationId) return; trackEvent("org_switched", { success: true }); window.location.href = `/${organization.slug}`; }; return AsyncResult.match(organizations, { onInitial: () => Loading…, onFailure: () => Failed to load organizations, onSuccess: ({ value }) => value.organizations.length === 0 ? ( No organizations ) : ( <> {value.organizations.map((organization: { id: string; name: string; slug: string }) => { const isActive = organization.id === props.activeOrganizationId; return ( { handleSwitch(organization); }} className="text-xs" > {organization.name} {isActive && } ); })} ), }); } export function OrgMenuSlot() { const auth = useAuth(); const [createOrganizationOpen, setCreateOrganizationOpen] = useState(false); const suggestedOrganizationName = auth.status === "authenticated" && auth.user.name?.trim() !== "" && auth.user.name != null ? `${auth.user.name}'s Organization` : "New Organization"; const form = useCreateOrganizationForm({ defaultName: suggestedOrganizationName, // Land on the new org's URL root — a reload would keep the old slug and // the slug gate would switch the session right back. onSuccess: (org) => { window.location.href = `/${org.slug}`; }, }); if (auth.status !== "authenticated") return null; const openCreateOrganization = () => { form.reset(suggestedOrganizationName); setCreateOrganizationOpen(true); }; return ( <> Organization {auth.organization?.name ?? "No organization"} { event.preventDefault(); openCreateOrganization(); }} > Create organization { setCreateOrganizationOpen(open); if (!open) form.reset(suggestedOrganizationName); }} > Create organization Add another organization under your current account and switch into it immediately. { form.setName(name); if (form.error) form.setError(null); }} error={form.error} onSubmit={() => void form.submit()} /> ); }