From b34a8d116b6cafd8d61224756ccbc45def733b61 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 14 Aug 2026 12:18:55 -0400 Subject: [PATCH] improve org selector --- src/components/OrgPicker.tsx | 137 ++++++++++++ src/components/OrgSelector.tsx | 203 ++++-------------- .../resource-launcher/LauncherOrgSelector.tsx | 140 ++---------- 3 files changed, 193 insertions(+), 287 deletions(-) create mode 100644 src/components/OrgPicker.tsx diff --git a/src/components/OrgPicker.tsx b/src/components/OrgPicker.tsx new file mode 100644 index 000000000..3113198fc --- /dev/null +++ b/src/components/OrgPicker.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useUserContext } from "@app/hooks/useUserContext"; +import { cn } from "@app/lib/cn"; +import { build } from "@server/build"; +import { ListUserOrgsResponse } from "@server/routers/org"; +import { CheckIcon, Plus } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { usePathname, useRouter } from "next/navigation"; +import { useMemo, useState, type ReactNode } from "react"; + +export type OrgPickerProps = { + orgId?: string; + orgs?: ListUserOrgsResponse["orgs"]; + contentClassName?: string; + sideOffset?: number; + children: ReactNode; +}; + +export function OrgPicker({ + orgId, + orgs, + contentClassName, + sideOffset = 0, + children +}: OrgPickerProps) { + const [open, setOpen] = useState(false); + const router = useRouter(); + const pathname = usePathname(); + const t = useTranslations(); + const { env } = useEnvContext(); + const { user } = useUserContext(); + + let canCreateOrg = !env.flags.disableUserCreateOrg || user.serverAdmin; + if (build === "saas" && user.type !== "internal") { + canCreateOrg = false; + } + + const sortedOrgs = useMemo(() => { + if (!orgs?.length) { + return orgs ?? []; + } + return [...orgs].sort((a, b) => { + const aPrimary = Boolean(a.isPrimaryOrg); + const bPrimary = Boolean(b.isPrimaryOrg); + if (aPrimary && !bPrimary) return -1; + if (!aPrimary && bPrimary) return 1; + return 0; + }); + }, [orgs]); + + function selectOrg(nextOrgId: string) { + setOpen(false); + const newPath = pathname.includes("/settings/") + ? pathname.replace(/^\/[^/]+/, `/${nextOrgId}`) + : `/${nextOrgId}`; + router.push(newPath); + } + + return ( + + {children} + + + + + {t("orgNotFound2")} + + {sortedOrgs.map((org) => ( + selectOrg(org.orgId)} + > + +
+ + {org.name} + + + {org.orgId} + {org.isPrimaryOrg + ? ` ยท ${t("primary")}` + : ""} + +
+
+ ))} +
+
+
+ {canCreateOrg && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/OrgSelector.tsx b/src/components/OrgSelector.tsx index 79f2fad7d..9742fdb90 100644 --- a/src/components/OrgSelector.tsx +++ b/src/components/OrgSelector.tsx @@ -1,199 +1,76 @@ "use client"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; +import { OrgPicker } from "@app/components/OrgPicker"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; -import { Badge } from "@app/components/ui/badge"; -import { useEnvContext } from "@app/hooks/useEnvContext"; import { cn } from "@app/lib/cn"; import { ListUserOrgsResponse } from "@server/routers/org"; -import { Check, ChevronsUpDown, Plus, Building2, Users } from "lucide-react"; -import { Button } from "@app/components/ui/button"; -import { usePathname, useRouter } from "next/navigation"; -import { useMemo, useState } from "react"; -import { useUserContext } from "@app/hooks/useUserContext"; +import { Building2, ChevronsUpDown } from "lucide-react"; import { useTranslations } from "next-intl"; -import { build } from "@server/build"; -interface OrgSelectorProps { +type OrgSelectorProps = { orgId?: string; orgs?: ListUserOrgsResponse["orgs"]; isCollapsed?: boolean; -} +}; export function OrgSelector({ orgId, orgs, isCollapsed = false }: OrgSelectorProps) { - const { user } = useUserContext(); - const [open, setOpen] = useState(false); - const router = useRouter(); - const pathname = usePathname(); - const { env } = useEnvContext(); const t = useTranslations(); - const selectedOrg = orgs?.find((org) => org.orgId === orgId); - let canCreateOrg = !env.flags.disableUserCreateOrg || user.serverAdmin; - if (build === "saas" && user.type !== "internal") { - canCreateOrg = false; - } - - const sortedOrgs = useMemo(() => { - if (!orgs?.length) return orgs ?? []; - return [...orgs].sort((a, b) => { - const aPrimary = Boolean(a.isPrimaryOrg); - const bPrimary = Boolean(b.isPrimaryOrg); - if (aPrimary && !bPrimary) return -1; - if (!aPrimary && bPrimary) return 1; - return 0; - }); - }, [orgs]); - - const orgSelectorContent = ( - - -
- {isCollapsed ? ( - - ) : ( -
-
-
- - {t("org")} - - - {selectedOrg?.name || t("noneSelected")} - -
-
- -
- )} -
-
- +
- - - - -
- {t("orgNotFound2")} + {isCollapsed ? ( + + ) : ( +
+
+
+ + {t("org")} + + + {selectedOrg?.name || t("noneSelected")} +
- - - {sortedOrgs.map((org) => ( - { - setOpen(false); - const newPath = pathname.includes( - "/settings/" - ) - ? pathname.replace( - /^\/[^/]+/, - `/${org.orgId}` - ) - : `/${org.orgId}`; - router.push(newPath); - }} - className="mx-1 rounded-md py-1.5 h-auto min-h-0" - > -
- -
-
- - {org.name} - -
- - {org.orgId} - - {org.isPrimaryOrg && ( - - {t("primary")} - - )} -
-
- -
- ))} -
- - - {canCreateOrg && ( -
- +
+
)} - - +
+ ); if (isCollapsed) { return ( - - {orgSelectorContent} - + {picker}

@@ -209,5 +86,5 @@ export function OrgSelector({ ); } - return orgSelectorContent; + return picker; } diff --git a/src/components/resource-launcher/LauncherOrgSelector.tsx b/src/components/resource-launcher/LauncherOrgSelector.tsx index 04773d2de..80510d77e 100644 --- a/src/components/resource-launcher/LauncherOrgSelector.tsx +++ b/src/components/resource-launcher/LauncherOrgSelector.tsx @@ -1,28 +1,10 @@ "use client"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { cn } from "@app/lib/cn"; -import { ListUserOrgsResponse } from "@server/routers/org"; -import { Check, ChevronDown, Plus } from "lucide-react"; -import { usePathname, useRouter } from "next/navigation"; -import { useMemo, useState } from "react"; -import { useTranslations } from "next-intl"; +import { OrgPicker } from "@app/components/OrgPicker"; import { Button } from "@app/components/ui/button"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useUserContext } from "@app/hooks/useUserContext"; -import { build } from "@server/build"; +import { ListUserOrgsResponse } from "@server/routers/org"; +import { ChevronDown } from "lucide-react"; +import { useTranslations } from "next-intl"; type LauncherOrgSelectorProps = { orgId?: string; @@ -30,111 +12,21 @@ type LauncherOrgSelectorProps = { }; export function LauncherOrgSelector({ orgId, orgs }: LauncherOrgSelectorProps) { - const [open, setOpen] = useState(false); - const router = useRouter(); - const pathname = usePathname(); const t = useTranslations(); - const { env } = useEnvContext(); - const { user } = useUserContext(); - const selectedOrg = orgs?.find((org) => org.orgId === orgId); - let canCreateOrg = !env.flags.disableUserCreateOrg || user.serverAdmin; - if (build === "saas" && user.type !== "internal") { - canCreateOrg = false; - } - - const sortedOrgs = useMemo(() => { - if (!orgs?.length) { - return orgs ?? []; - } - return [...orgs].sort((a, b) => { - const aPrimary = Boolean(a.isPrimaryOrg); - const bPrimary = Boolean(b.isPrimaryOrg); - if (aPrimary && !bPrimary) { - return -1; - } - if (!aPrimary && bPrimary) { - return 1; - } - return 0; - }); - }, [orgs]); - return ( - - - - - - - - - {t("orgNotFound2")} - - {sortedOrgs.map((org) => ( - { - setOpen(false); - const newPath = pathname.includes( - "/settings/" - ) - ? pathname.replace( - /^\/[^/]+/, - `/${org.orgId}` - ) - : `/${org.orgId}`; - router.push(newPath); - }} - > -

- - {org.name} - - - {org.orgId} - -
- - - ))} - - - - {canCreateOrg && ( -
- -
- )} - - + + + ); }