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
>
);
}