"use client"; import type { ReactElement, SyntheticEvent } from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { UserPlus, UserX } from "lucide-react"; import { WorkspaceMemberRole } from "@mosaic/shared"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { addWorkspaceMember, createWorkspace, fetchUserWorkspaces, fetchWorkspaceMembers, removeWorkspaceMember, type UserWorkspace, type WorkspaceMemberEntry, } from "@/lib/api/workspaces"; import { fetchAdminUsers, type AdminUser } from "@/lib/api/admin"; function getErrorMessage(error: unknown, fallback: string): string { if (error instanceof Error) { return error.message; } return fallback; } function toRoleLabel(role: WorkspaceMemberRole): string { return `${role.charAt(0)}${role.slice(1).toLowerCase()}`; } interface RemoveMemberTarget { userId: string; email: string; } const ROLE_BADGE_CLASS: Record = { [WorkspaceMemberRole.OWNER]: "border-purple-200 bg-purple-50 text-purple-700", [WorkspaceMemberRole.ADMIN]: "border-blue-200 bg-blue-50 text-blue-700", [WorkspaceMemberRole.MEMBER]: "border-green-200 bg-green-50 text-green-700", [WorkspaceMemberRole.GUEST]: "border-gray-200 bg-gray-50 text-gray-700", }; export default function WorkspacesPage(): ReactElement { const [workspaces, setWorkspaces] = useState([]); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [isCreating, setIsCreating] = useState(false); const [newWorkspaceName, setNewWorkspaceName] = useState(""); const [createError, setCreateError] = useState(null); const [members, setMembers] = useState([]); const [isMembersLoading, setIsMembersLoading] = useState(false); const [membersError, setMembersError] = useState(null); const [isAddMemberOpen, setIsAddMemberOpen] = useState(false); const [isAddingMember, setIsAddingMember] = useState(false); const [addMemberError, setAddMemberError] = useState(null); const [memberUserId, setMemberUserId] = useState(""); const [memberRole, setMemberRole] = useState(WorkspaceMemberRole.MEMBER); const [availableUsers, setAvailableUsers] = useState([]); const [isLoadingUsers, setIsLoadingUsers] = useState(false); const [removeTarget, setRemoveTarget] = useState(null); const [isRemovingMember, setIsRemovingMember] = useState(false); const selectedWorkspace = useMemo( () => workspaces.find((workspace) => workspace.id === selectedWorkspaceId) ?? null, [selectedWorkspaceId, workspaces] ); const loadWorkspaces = useCallback(async (): Promise => { setIsLoading(true); try { const data = await fetchUserWorkspaces(); setWorkspaces(data); setLoadError(null); setSelectedWorkspaceId((current) => { if (current && data.some((workspace) => workspace.id === current)) { return current; } return data[0]?.id ?? null; }); } catch (error) { setLoadError(getErrorMessage(error, "Failed to load workspaces")); setSelectedWorkspaceId(null); } finally { setIsLoading(false); } }, []); const loadMembers = useCallback(async (workspaceId: string): Promise => { setIsMembersLoading(true); setMembersError(null); try { const data = await fetchWorkspaceMembers(workspaceId); setMembers(data); } catch (error) { setMembersError(getErrorMessage(error, "Failed to load workspace members")); setMembers([]); } finally { setIsMembersLoading(false); } }, []); useEffect(() => { void loadWorkspaces(); }, [loadWorkspaces]); useEffect(() => { if (!selectedWorkspaceId) { setMembers([]); setMembersError(null); return; } void loadMembers(selectedWorkspaceId); }, [loadMembers, selectedWorkspaceId]); const handleCreateWorkspace = async (event: SyntheticEvent): Promise => { event.preventDefault(); const workspaceName = newWorkspaceName.trim(); if (!workspaceName) { return; } setIsCreating(true); setCreateError(null); try { await createWorkspace({ name: workspaceName }); setNewWorkspaceName(""); await loadWorkspaces(); } catch (error) { setCreateError(getErrorMessage(error, "Failed to create workspace")); } finally { setIsCreating(false); } }; const eligibleUsers = useMemo(() => { const memberIds = new Set(members.map((member) => member.userId)); return availableUsers.filter((user) => !memberIds.has(user.id)); }, [availableUsers, members]); const loadAvailableUsers = useCallback(async (): Promise => { setIsLoadingUsers(true); try { const response = await fetchAdminUsers(1, 200); const activeUsers = response.data.filter((user) => user.deactivatedAt === null); setAvailableUsers(activeUsers); if (memberUserId && activeUsers.some((user) => user.id === memberUserId)) { return; } const memberIds = new Set(members.map((member) => member.userId)); const firstEligible = activeUsers.find((user) => !memberIds.has(user.id)); setMemberUserId(firstEligible?.id ?? ""); } catch (error) { setAddMemberError(getErrorMessage(error, "Failed to load users for member assignment")); setAvailableUsers([]); setMemberUserId(""); } finally { setIsLoadingUsers(false); } }, [memberUserId, members]); const openAddMemberDialog = async (): Promise => { setAddMemberError(null); setMemberRole(WorkspaceMemberRole.MEMBER); setIsAddMemberOpen(true); await loadAvailableUsers(); }; const handleAddMember = async (event: SyntheticEvent): Promise => { event.preventDefault(); if (!selectedWorkspaceId) { setAddMemberError("Select a workspace before adding members."); return; } if (!memberUserId) { setAddMemberError("Select a user to add."); return; } setIsAddingMember(true); setAddMemberError(null); try { await addWorkspaceMember(selectedWorkspaceId, { userId: memberUserId, role: memberRole, }); setIsAddMemberOpen(false); await loadMembers(selectedWorkspaceId); } catch (error) { setAddMemberError(getErrorMessage(error, "Failed to add member")); } finally { setIsAddingMember(false); } }; const handleRemoveMember = async (): Promise => { if (!selectedWorkspaceId || !removeTarget) { return; } setIsRemovingMember(true); try { await removeWorkspaceMember(selectedWorkspaceId, removeTarget.userId); setRemoveTarget(null); await loadMembers(selectedWorkspaceId); } catch (error) { setMembersError(getErrorMessage(error, "Failed to remove member")); } finally { setIsRemovingMember(false); } }; return (

Workspaces

Manage workspaces and workspace members

← Back to Settings
Create New Workspace Create a workspace for a new team or project.
{ setNewWorkspaceName(event.target.value); }} placeholder="Enter workspace name..." disabled={isCreating} />
{createError !== null ? (

{createError}

) : null}
{loadError !== null ? (

{loadError}

) : null}
Your Workspaces ({isLoading ? "..." : workspaces.length}) Click a workspace to manage its members. {isLoading ? (

Loading workspaces...

) : workspaces.length === 0 ? (

No workspaces yet. Create one to begin.

) : ( workspaces.map((workspace) => { const isSelected = selectedWorkspaceId === workspace.id; return ( ); }) )}
{selectedWorkspace ? `${selectedWorkspace.name} Members` : "Workspace Members"} {selectedWorkspace ? "Manage member roles and access for this workspace." : "Select a workspace to view its members."}
{ if (!open && !isAddingMember) { setIsAddMemberOpen(false); setAddMemberError(null); } }} > Add Workspace Member Add an existing user to {selectedWorkspace?.name ?? "this workspace"}.
{ void handleAddMember(event); }} className="space-y-4" >
{addMemberError !== null ? (

{addMemberError}

) : null}
{selectedWorkspace === null ? (

Select a workspace to view members.

) : membersError !== null ? (

{membersError}

) : isMembersLoading ? (

Loading members...

) : members.length === 0 ? (

No members found for this workspace.

) : ( members.map((member) => (

{member.user.name ?? "Unnamed User"}

{member.user.email}

{toRoleLabel(member.role)}
)) )}
{ if (!open && !isRemovingMember) { setRemoveTarget(null); } }} > Remove Workspace Member Remove {removeTarget?.email} from {selectedWorkspace?.name}? They will lose access to this workspace. Cancel { void handleRemoveMember(); }} > {isRemovingMember ? "Removing..." : "Remove"}
); }