- Open registration to all users (remove invite-only gate) - Add domain auto-join: new users matching org allowedDomains get auto-added - Track active workspace via activeOrgId on User model - Refactor JWT callback to resolve active org from all memberships - Add org switch, list, and create-personal API endpoints - Add workspace-setup page for users without an org - Build OrgSwitcher dropdown in sidebar header - Add allowed email domains management to org settings Made-with: Cursor
138 lines
3.9 KiB
TypeScript
138 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Check, ChevronsUpDown, Plus } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { useUserProfile } from "@/lib/user-profile";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
Tooltip,
|
|
TooltipTrigger,
|
|
TooltipContent,
|
|
} from "@/components/ui/tooltip";
|
|
|
|
type OrgListItem = {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
type: string;
|
|
role: string;
|
|
};
|
|
|
|
export function OrgSwitcher({ collapsed }: { collapsed: boolean }) {
|
|
const { orgId, orgName, switchOrg } = useUserProfile();
|
|
const router = useRouter();
|
|
const [orgs, setOrgs] = React.useState<OrgListItem[]>([]);
|
|
const [loading, setLoading] = React.useState(false);
|
|
|
|
const fetchOrgs = React.useCallback(async () => {
|
|
try {
|
|
const res = await fetch("/api/org/list");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setOrgs(data.orgs ?? []);
|
|
}
|
|
} catch {}
|
|
}, []);
|
|
|
|
const initial = orgName
|
|
? orgName
|
|
.split(/\s+/)
|
|
.map((w: string) => w[0])
|
|
.join("")
|
|
.slice(0, 2)
|
|
.toUpperCase()
|
|
: "?";
|
|
|
|
const trigger = (
|
|
<DropdownMenuTrigger
|
|
className={cn(
|
|
"flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-sidebar-accent/50",
|
|
collapsed && "justify-center px-0"
|
|
)}
|
|
onClick={fetchOrgs}
|
|
>
|
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-xs font-bold text-primary">
|
|
{initial}
|
|
</div>
|
|
{!collapsed && (
|
|
<>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-semibold leading-tight text-sidebar-foreground">
|
|
{orgName || "No workspace"}
|
|
</p>
|
|
</div>
|
|
<ChevronsUpDown className="size-4 shrink-0 text-sidebar-foreground/40" />
|
|
</>
|
|
)}
|
|
</DropdownMenuTrigger>
|
|
);
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
{collapsed ? (
|
|
<Tooltip>
|
|
<TooltipTrigger render={<div />}>{trigger}</TooltipTrigger>
|
|
<TooltipContent side="right" sideOffset={8}>
|
|
{orgName || "No workspace"}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
) : (
|
|
trigger
|
|
)}
|
|
|
|
<DropdownMenuContent
|
|
side={collapsed ? "right" : "bottom"}
|
|
align="start"
|
|
sideOffset={collapsed ? 8 : 4}
|
|
className="w-64"
|
|
>
|
|
{orgs.map((org) => (
|
|
<DropdownMenuItem
|
|
key={org.id}
|
|
className="flex items-center gap-2"
|
|
disabled={loading}
|
|
onClick={async () => {
|
|
if (org.id === orgId) return;
|
|
setLoading(true);
|
|
await switchOrg(org.id);
|
|
setLoading(false);
|
|
}}
|
|
>
|
|
<div className="flex size-6 shrink-0 items-center justify-center rounded bg-primary/10 text-[10px] font-bold text-primary">
|
|
{org.name
|
|
.split(/\s+/)
|
|
.map((w) => w[0])
|
|
.join("")
|
|
.slice(0, 2)
|
|
.toUpperCase()}
|
|
</div>
|
|
<span className="flex-1 truncate">{org.name}</span>
|
|
{org.id === orgId && (
|
|
<Check className="size-4 shrink-0 text-primary" />
|
|
)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
|
|
{orgs.length > 0 && <DropdownMenuSeparator />}
|
|
|
|
<DropdownMenuItem
|
|
className="flex items-center gap-2"
|
|
onClick={() => router.push("/onboarding")}
|
|
>
|
|
<div className="flex size-6 items-center justify-center">
|
|
<Plus className="size-4 text-muted-foreground" />
|
|
</div>
|
|
<span>Create new workspace</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|