Unified · Split
| @@ -1,10 +1,5 @@ | |||
| 1 | import { AppShell } from "@/components/app-shell"; | ||
| 2 | import { ApprovalsView } from "@/components/approvals-view"; | 1 | import { ApprovalsView } from "@/components/approvals-view"; |
| 3 | 2 | ||
| 4 | export default function ApprovalsPage() { | 3 | export default function ApprovalsPage() { |
| 5 | return ( | 4 | return <ApprovalsView />; |
| 6 | <AppShell> | ||
| 7 | <ApprovalsView /> | ||
| 8 | </AppShell> | ||
| 9 | ); | ||
| 10 | } | 5 | } |
| @@ -1,10 +1,5 @@ | |||
| 1 | import { AppShell } from "@/components/app-shell"; | ||
| 2 | import { BillingView } from "@/components/billing-view"; | 1 | import { BillingView } from "@/components/billing-view"; |
| 3 | 2 | ||
| 4 | export default function BillingPage() { | 3 | export default function BillingPage() { |
| 5 | return ( | 4 | return <BillingView />; |
| 6 | <AppShell> | ||
| 7 | <BillingView /> | ||
| 8 | </AppShell> | ||
| 9 | ); | ||
| 10 | } | 5 | } |
| @@ -1,6 +1,7 @@ | |||
| 1 | import type { Metadata } from "next"; | 1 | import type { Metadata } from "next"; |
| 2 | import { JetBrains_Mono, Inter } from "next/font/google"; | 2 | import { JetBrains_Mono, Inter } from "next/font/google"; |
| 3 | import { Toaster } from "sonner"; | 3 | import { Toaster } from "sonner"; |
| 4 | import { AppShell } from "@/components/app-shell"; | ||
| 4 | import { AppConvexProvider } from "@/lib/convex-provider"; | 5 | import { AppConvexProvider } from "@/lib/convex-provider"; |
| 5 | import { QueryProvider } from "@/lib/query-provider"; | 6 | import { QueryProvider } from "@/lib/query-provider"; |
| 6 | import { SessionProvider } from "@/lib/session-context"; | 7 | import { SessionProvider } from "@/lib/session-context"; |
| @@ -34,7 +35,7 @@ export default function RootLayout({ · 27 unmodified lines | |||
| 34 | <AppConvexProvider> | 35 | <AppConvexProvider> |
| 35 | <QueryProvider> | 36 | <QueryProvider> |
| 36 | <SessionProvider> | 37 | <SessionProvider> |
| 37 | {children} | 38 | <AppShell>{children}</AppShell> |
| 38 | </SessionProvider> | 39 | </SessionProvider> |
| 39 | </QueryProvider> | 40 | </QueryProvider> |
| 40 | </AppConvexProvider> | 41 | </AppConvexProvider> |
| @@ -1,10 +1,5 @@ | |||
| 1 | import { AppShell } from "@/components/app-shell"; | ||
| 2 | import { MembersView } from "@/components/members-view"; | 1 | import { MembersView } from "@/components/members-view"; |
| 3 | 2 | ||
| 4 | export default function MembersPage() { | 3 | export default function MembersPage() { |
| 5 | return ( | 4 | return <MembersView />; |
| 6 | <AppShell> | ||
| 7 | <MembersView /> | ||
| 8 | </AppShell> | ||
| 9 | ); | ||
| 10 | } | 5 | } |
| @@ -1,10 +1,5 @@ | |||
| 1 | import { AppShell } from "@/components/app-shell"; | ||
| 2 | import { DashboardView } from "@/components/dashboard-view"; | 1 | import { DashboardView } from "@/components/dashboard-view"; |
| 3 | 2 | ||
| 4 | export default function DashboardPage() { | 3 | export default function DashboardPage() { |
| 5 | return ( | 4 | return <DashboardView />; |
| 6 | <AppShell> | ||
| 7 | <DashboardView /> | ||
| 8 | </AppShell> | ||
| 9 | ); | ||
| 10 | } | 5 | } |
| @@ -1,13 +1,10 @@ | |||
| 1 | import { Suspense } from "react"; | 1 | import { Suspense } from "react"; |
| 2 | import { AppShell } from "@/components/app-shell"; | ||
| 3 | import { TasksView } from "@/components/tasks-view"; | 2 | import { TasksView } from "@/components/tasks-view"; |
| 4 | 3 | ||
| 5 | export default function TasksPage() { | 4 | export default function TasksPage() { |
| 6 | return ( | 5 | return ( |
| 7 | <AppShell> | 6 | <Suspense> |
| 8 | <Suspense> | 7 | <TasksView /> |
| 9 | <TasksView /> | 8 | </Suspense> |
| 10 | </Suspense> | ||
| 11 | </AppShell> | ||
| 12 | ); | 9 | ); |
| 13 | } | 10 | } |
| @@ -1,10 +1,5 @@ | |||
| 1 | import { AppShell } from "@/components/app-shell"; | ||
| 2 | import { ToolsView } from "@/components/tools-view"; | 1 | import { ToolsView } from "@/components/tools-view"; |
| 3 | 2 | ||
| 4 | export default function ToolsPage() { | 3 | export default function ToolsPage() { |
| 5 | return ( | 4 | return <ToolsView />; |
| 6 | <AppShell> | ||
| 7 | <ToolsView /> | ||
| 8 | </AppShell> | ||
| 9 | ); | ||
| 10 | } | 5 | } |
| @@ -84,8 +84,6 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 83 unmodified lines | |||
| 84 | context, | 84 | context, |
| 85 | mode, | 85 | mode, |
| 86 | clientConfig, | 86 | clientConfig, |
| 87 | organizations, | ||
| 88 | selectedOrganizationId, | ||
| 89 | workspaces, | 87 | workspaces, |
| 90 | switchWorkspace, | 88 | switchWorkspace, |
| 91 | creatingWorkspace, | 89 | creatingWorkspace, |
| @@ -99,25 +97,9 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 7 unmodified lines | |||
| 99 | const activeWorkspace = context | 97 | const activeWorkspace = context |
| 100 | ? workspaces.find((workspace) => workspace.id === context.workspaceId) | 98 | ? workspaces.find((workspace) => workspace.id === context.workspaceId) |
| 101 | : null; | 99 | : null; |
| 102 | const currentOrganizationId = activeWorkspace?.organizationId ?? selectedOrganizationId; | ||
| 103 | const activeOrganization = | ||
| 104 | currentOrganizationId | ||
| 105 | ? organizations.find((organization) => organization.id === currentOrganizationId) ?? null | ||
| 106 | : null; | ||
| 107 | const activeWorkspaceLabel = activeWorkspace?.name ?? (mode === "guest" ? "Guest Workspace" : "Select workspace"); | 100 | const activeWorkspaceLabel = activeWorkspace?.name ?? (mode === "guest" ? "Guest Workspace" : "Select workspace"); |
| 108 | const activeOrganizationLabel = activeOrganization?.name | ||
| 109 | ?? (activeWorkspace?.kind === "organization" ? "Organization" : mode === "guest" ? "Guest" : "Personal"); | ||
| 110 | const activeWorkspaceInitial = (activeWorkspaceLabel[0] ?? "W").toUpperCase(); | 101 | const activeWorkspaceInitial = (activeWorkspaceLabel[0] ?? "W").toUpperCase(); |
| 111 | 102 | ||
| 112 | const organizationGroups = organizations | ||
| 113 | .map((organization) => ({ | ||
| 114 | organization, | ||
| 115 | workspaces: workspaces.filter((workspace) => workspace.organizationId === organization.id), | ||
| 116 | })) | ||
| 117 | .filter((group) => group.workspaces.length > 0); | ||
| 118 | const personalWorkspaces = workspaces.filter((workspace) => workspace.organizationId === null); | ||
| 119 | const showOrganizationHeaders = organizationGroups.length > 1; | ||
| 120 | |||
| 121 | const openCreateWorkspace = () => { | 103 | const openCreateWorkspace = () => { |
| 122 | setCreateError(null); | 104 | setCreateError(null); |
| 123 | setNewWorkspaceName(""); | 105 | setNewWorkspaceName(""); |
| @@ -172,9 +154,6 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 48 unmodified lines | |||
| 172 | )} | 154 | )} |
| 173 | <span className="min-w-0"> | 155 | <span className="min-w-0"> |
| 174 | <span className="truncate block">{activeWorkspaceLabel}</span> | 156 | <span className="truncate block">{activeWorkspaceLabel}</span> |
| 175 | {mode === "workos" ? ( | ||
| 176 | <span className="truncate block text-[10px] text-muted-foreground">{activeOrganizationLabel}</span> | ||
| 177 | ) : null} | ||
| 178 | </span> | 157 | </span> |
| 179 | </span> | 158 | </span> |
| 180 | <ChevronsUpDown className="h-3.5 w-3.5 text-muted-foreground" /> | 159 | <ChevronsUpDown className="h-3.5 w-3.5 text-muted-foreground" /> |
| @@ -184,75 +163,32 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 3 unmodified lines | |||
| 184 | {mode === "workos" | 163 | {mode === "workos" |
| 185 | ? ( | 164 | ? ( |
| 186 | <> | 165 | <> |
| 187 | {organizationGroups.map((group, index) => ( | 166 | {workspaces.map((workspace) => { |
| 188 | <div key={group.organization.id}> | 167 | const isActive = workspace.id === context?.workspaceId; |
| 189 | {showOrganizationHeaders ? ( | 168 | return ( |
| 190 | <DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground"> | 169 | <DropdownMenuItem |
| 191 | {group.organization.name} | 170 | key={workspace.id} |
| 192 | </DropdownMenuLabel> | 171 | onSelect={() => switchWorkspace(workspace.id)} |
| 193 | ) : null} | 172 | className="text-xs" |
| 194 | {group.workspaces.map((workspace) => { | 173 | > |
| 195 | const isActive = workspace.id === context?.workspaceId; | 174 | <Check className={cn("mr-2 h-3.5 w-3.5", isActive ? "opacity-100" : "opacity-0")} /> |
| 196 | return ( | 175 | {workspace.iconUrl ? ( |
| 197 | <DropdownMenuItem | 176 | <img |
| 198 | key={workspace.id} | 177 | src={workspace.iconUrl} |
| 199 | onSelect={() => switchWorkspace(workspace.id)} | 178 | alt={workspace.name} |
| 200 | className="text-xs" | 179 | className="mr-2 h-4 w-4 rounded-sm border border-border object-cover" |
| 201 | > | 180 | /> |
| 202 | <Check className={cn("mr-2 h-3.5 w-3.5", isActive ? "opacity-100" : "opacity-0")} /> | 181 | ) : ( |
| 203 | {workspace.iconUrl ? ( | 182 | <span className="mr-2 h-4 w-4 rounded-sm border border-border bg-muted text-[9px] font-semibold flex items-center justify-center text-muted-foreground"> |
| 204 | <img | 183 | {(workspace.name[0] ?? "W").toUpperCase()} |
| 205 | src={workspace.iconUrl} | 184 | </span> |
| 206 | alt={workspace.name} | 185 | )} |
| 207 | className="mr-2 h-4 w-4 rounded-sm border border-border object-cover" | 186 | <span className="truncate">{workspace.name}</span> |
| 208 | /> | 187 | </DropdownMenuItem> |
| 209 | ) : ( | 188 | ); |
| 210 | <span className="mr-2 h-4 w-4 rounded-sm border border-border bg-muted text-[9px] font-semibold flex items-center justify-center text-muted-foreground"> | 189 | })} |
| 211 | {(workspace.name[0] ?? "W").toUpperCase()} | ||
| 212 | </span> | ||
| 213 | )} | ||
| 214 | <span className="truncate">{workspace.name}</span> | ||
| 215 | </DropdownMenuItem> | ||
| 216 | ); | ||
| 217 | })} | ||
| 218 | {showOrganizationHeaders && index < organizationGroups.length - 1 ? <DropdownMenuSeparator /> : null} | ||
| 219 | </div> | ||
| 220 | ))} | ||
| 221 | |||
| 222 | {personalWorkspaces.length > 0 ? ( | ||
| 223 | <> | ||
| 224 | {organizationGroups.length > 0 ? <DropdownMenuSeparator /> : null} | ||
| 225 | <DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground"> | ||
| 226 | Personal | ||
| 227 | </DropdownMenuLabel> | ||
| 228 | {personalWorkspaces.map((workspace) => { | ||
| 229 | const isActive = workspace.id === context?.workspaceId; | ||
| 230 | return ( | ||
| 231 | <DropdownMenuItem | ||
| 232 | key={workspace.id} | ||
| 233 | onSelect={() => switchWorkspace(workspace.id)} | ||
| 234 | className="text-xs" | ||
| 235 | > | ||
| 236 | <Check className={cn("mr-2 h-3.5 w-3.5", isActive ? "opacity-100" : "opacity-0")} /> | ||
| 237 | {workspace.iconUrl ? ( | ||
| 238 | <img | ||
| 239 | src={workspace.iconUrl} | ||
| 240 | alt={workspace.name} | ||
| 241 | className="mr-2 h-4 w-4 rounded-sm border border-border object-cover" | ||
| 242 | /> | ||
| 243 | ) : ( | ||
| 244 | <span className="mr-2 h-4 w-4 rounded-sm border border-border bg-muted text-[9px] font-semibold flex items-center justify-center text-muted-foreground"> | ||
| 245 | {(workspace.name[0] ?? "W").toUpperCase()} | ||
| 246 | </span> | ||
| 247 | )} | ||
| 248 | <span className="truncate">{workspace.name}</span> | ||
| 249 | </DropdownMenuItem> | ||
| 250 | ); | ||
| 251 | })} | ||
| 252 | </> | ||
| 253 | ) : null} | ||
| 254 | 190 | ||
| 255 | {organizationGroups.length === 0 && personalWorkspaces.length === 0 ? ( | 191 | {workspaces.length === 0 ? ( |
| 256 | <DropdownMenuItem disabled className="text-xs"> | 192 | <DropdownMenuItem disabled className="text-xs"> |
| 257 | No workspaces | 193 | No workspaces |
| 258 | </DropdownMenuItem> | 194 | </DropdownMenuItem> |
| @@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; · 7 unmodified lines | |||
| 8 | import { useSession } from "@/lib/session-context"; | 8 | import { useSession } from "@/lib/session-context"; |
| 9 | 9 | ||
| 10 | export function BillingView() { | 10 | export function BillingView() { |
| 11 | const { context, organizations, selectedOrganizationId, workspaces } = useSession(); | 11 | const { context, organizations, workspaces } = useSession(); |
| 12 | const [actionState, setActionState] = useState<"idle" | "running" | "error">("idle"); | 12 | const [actionState, setActionState] = useState<"idle" | "running" | "error">("idle"); |
| 13 | const [actionMessage, setActionMessage] = useState<string | null>(null); | 13 | const [actionMessage, setActionMessage] = useState<string | null>(null); |
| 14 | 14 | ||
| @@ -18,8 +18,8 @@ export function BillingView() { · 3 unmodified lines | |||
| 18 | const activeWorkspace = context | 18 | const activeWorkspace = context |
| 19 | ? workspaces.find((workspace) => workspace.id === context.workspaceId) ?? null | 19 | ? workspaces.find((workspace) => workspace.id === context.workspaceId) ?? null |
| 20 | : null; | 20 | : null; |
| 21 | const effectiveOrganizationId = derivedOrganizationId ?? selectedOrganizationId; | 21 | const effectiveOrganizationId = derivedOrganizationId; |
| 22 | const hasLegacyOrganizationWorkspace = Boolean(activeWorkspace) && !effectiveOrganizationId; | 22 | const hasLegacyOrganizationWorkspace = activeWorkspace?.kind === "organization" && !effectiveOrganizationId; |
| 23 | 23 | ||
| 24 | const activeOrganization = useMemo( | 24 | const activeOrganization = useMemo( |
| 25 | () => organizations.find((organization) => organization.id === effectiveOrganizationId) ?? null, | 25 | () => organizations.find((organization) => organization.id === effectiveOrganizationId) ?? null, |
| @@ -40,7 +40,7 @@ export function BillingView() { · 14 unmodified lines | |||
| 40 | <PageHeader title="Billing" description="Plan, seats, and subscription management" /> | 40 | <PageHeader title="Billing" description="Plan, seats, and subscription management" /> |
| 41 | <Card> | 41 | <Card> |
| 42 | <CardContent className="p-6 text-sm text-muted-foreground"> | 42 | <CardContent className="p-6 text-sm text-muted-foreground"> |
| 43 | Select an organization from the switcher to manage billing. | 43 | Select a workspace to manage billing. |
| 44 | </CardContent> | 44 | </CardContent> |
| 45 | </Card> | 45 | </Card> |
| 46 | </div> | 46 | </div> |
| @@ -53,8 +53,8 @@ export function BillingView() { · 6 unmodified lines | |||
| 53 | <PageHeader title="Billing" description="Plan, seats, and subscription management" /> | 53 | <PageHeader title="Billing" description="Plan, seats, and subscription management" /> |
| 54 | <Card> | 54 | <Card> |
| 55 | <CardContent className="p-6 text-sm text-muted-foreground"> | 55 | <CardContent className="p-6 text-sm text-muted-foreground"> |
| 56 | Billing is waiting for an organization record for this workspace. The active workspace | 56 | Billing is waiting for workspace provisioning to finish. This workspace is missing its |
| 57 | does not have a linked `organizationId` yet. | 57 | internal organization link. |
| 58 | </CardContent> | 58 | </CardContent> |
| 59 | </Card> | 59 | </Card> |
| 60 | </div> | 60 | </div> |
| @@ -1,6 +1,6 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | import { useMemo, useState } from "react"; | 3 | import { useState } from "react"; |
| 4 | import { useMutation, useQuery } from "convex/react"; | 4 | import { useMutation, useQuery } from "convex/react"; |
| 5 | import { Mail, UserMinus, Users } from "lucide-react"; | 5 | import { Mail, UserMinus, Users } from "lucide-react"; |
| 6 | import { PageHeader } from "@/components/page-header"; | 6 | import { PageHeader } from "@/components/page-header"; |
| @@ -18,8 +18,6 @@ export function MembersView() { · 11 unmodified lines | |||
| 18 | const { | 18 | const { |
| 19 | context, | 19 | context, |
| 20 | clientConfig, | 20 | clientConfig, |
| 21 | organizations, | ||
| 22 | selectedOrganizationId, | ||
| 23 | workspaces, | 21 | workspaces, |
| 24 | } = useSession(); | 22 | } = useSession(); |
| 25 | 23 | ||
| @@ -30,8 +28,7 @@ export function MembersView() { · 4 unmodified lines | |||
| 30 | ? workspaces.find((workspace) => workspace.id === context.workspaceId) ?? null | 28 | ? workspaces.find((workspace) => workspace.id === context.workspaceId) ?? null |
| 31 | : null; | 29 | : null; |
| 32 | const typedOrganizationId = derivedOrganizationId; | 30 | const typedOrganizationId = derivedOrganizationId; |
| 33 | const effectiveOrganizationId = typedOrganizationId ?? selectedOrganizationId; | 31 | const hasLegacyOrganizationWorkspace = activeWorkspace?.kind === "organization" && !typedOrganizationId; |
| 34 | const hasLegacyOrganizationWorkspace = Boolean(activeWorkspace) && !typedOrganizationId; | ||
| 35 | 32 | ||
| 36 | const members = useQuery( | 33 | const members = useQuery( |
| 37 | convexApi.organizationMembers.list, | 34 | convexApi.organizationMembers.list, |
| @@ -43,6 +40,13 @@ export function MembersView() { · 5 unmodified lines | |||
| 43 | const updateRole = useMutation(convexApi.organizationMembers.updateRole); | 40 | const updateRole = useMutation(convexApi.organizationMembers.updateRole); |
| 44 | const updateBillable = useMutation(convexApi.organizationMembers.updateBillable); | 41 | const updateBillable = useMutation(convexApi.organizationMembers.updateBillable); |
| 45 | const removeMember = useMutation(convexApi.organizationMembers.remove); | 42 | const removeMember = useMutation(convexApi.organizationMembers.remove); |
| 43 | const listInvites = useQuery( | ||
| 44 | convexApi.invites.list, | ||
| 45 | typedOrganizationId | ||
| 46 | ? { organizationId: typedOrganizationId, sessionId: context?.sessionId ?? undefined } | ||
| 47 | : "skip", | ||
| 48 | ); | ||
| 49 | const createInvite = useMutation(convexApi.invites.create); | ||
| 46 | 50 | ||
| 47 | const [inviteEmail, setInviteEmail] = useState(""); | 51 | const [inviteEmail, setInviteEmail] = useState(""); |
| 48 | const [inviteRole, setInviteRole] = useState<Role>("member"); | 52 | const [inviteRole, setInviteRole] = useState<Role>("member"); |
| @@ -50,20 +54,14 @@ export function MembersView() { · 1 unmodified line | |||
| 50 | const [inviteMessage, setInviteMessage] = useState<string | null>(null); | 54 | const [inviteMessage, setInviteMessage] = useState<string | null>(null); |
| 51 | const [busyMemberAccountId, setBusyMemberAccountId] = useState<string | null>(null); | 55 | const [busyMemberAccountId, setBusyMemberAccountId] = useState<string | null>(null); |
| 52 | 56 | ||
| 53 | const activeOrganization = useMemo( | ||
| 54 | () => organizations.find((organization) => organization.id === effectiveOrganizationId) ?? null, | ||
| 55 | [organizations, effectiveOrganizationId], | ||
| 56 | ); | ||
| 57 | |||
| 58 | const canManageMembers = activeOrganization | ||
| 59 | ? activeOrganization.role === "owner" || activeOrganization.role === "admin" | ||
| 60 | : false; | ||
| 61 | const canManageBilling = activeOrganization | ||
| 62 | ? activeOrganization.role === "owner" || activeOrganization.role === "admin" || activeOrganization.role === "billing_admin" | ||
| 63 | : false; | ||
| 64 | |||
| 65 | const memberItems = members?.items ?? []; | 57 | const memberItems = members?.items ?? []; |
| 66 | const inviteItems: Array<{ id: string; email: string; role: string; status: string }> = []; | 58 | const inviteItems = listInvites?.items ?? []; |
| 59 | const actorMembership = memberItems.find((member) => | ||
| 60 | context?.accountId ? String(member.accountId) === context.accountId : false, | ||
| 61 | ); | ||
| 62 | const actorRole = actorMembership?.role ?? null; | ||
| 63 | const canManageMembers = actorRole === "owner" || actorRole === "admin"; | ||
| 64 | const canManageBilling = actorRole === "owner" || actorRole === "admin" || actorRole === "billing_admin"; | ||
| 67 | 65 | ||
| 68 | const submitInvite = async () => { | 66 | const submitInvite = async () => { |
| 69 | if (!typedOrganizationId) { | 67 | if (!typedOrganizationId) { |
| @@ -72,10 +70,17 @@ export function MembersView() { · 2 unmodified lines | |||
| 72 | setInviteState("sending"); | 70 | setInviteState("sending"); |
| 73 | setInviteMessage(null); | 71 | setInviteMessage(null); |
| 74 | try { | 72 | try { |
| 75 | await new Promise((resolve) => setTimeout(resolve, 300)); | 73 | const result = await createInvite({ |
| 76 | const deliveryProvider = clientConfig?.invitesProvider ?? "local"; | 74 | organizationId: typedOrganizationId, |
| 77 | setInviteState("failed"); | 75 | email: inviteEmail.trim(), |
| 78 | setInviteMessage(`Invite creation via ${deliveryProvider} is not wired yet on backend.`); | 76 | role: inviteRole, |
| 77 | workspaceId: activeWorkspace?.docId ?? undefined, | ||
| 78 | sessionId: context?.sessionId ?? undefined, | ||
| 79 | }); | ||
| 80 | setInviteState("sent"); | ||
| 81 | const deliveryProvider = result.delivery.provider; | ||
| 82 | setInviteMessage(`Invite ${result.delivery.state} via ${deliveryProvider}.`); | ||
| 83 | setInviteEmail(""); | ||
| 79 | } catch (error) { | 84 | } catch (error) { |
| 80 | setInviteState("failed"); | 85 | setInviteState("failed"); |
| 81 | setInviteMessage(error instanceof Error ? error.message : "Failed to send invite"); | 86 | setInviteMessage(error instanceof Error ? error.message : "Failed to send invite"); |
| @@ -88,7 +93,7 @@ export function MembersView() { · 6 unmodified lines | |||
| 88 | <PageHeader title="Members" description="Manage organization membership and invites" /> | 93 | <PageHeader title="Members" description="Manage organization membership and invites" /> |
| 89 | <Card> | 94 | <Card> |
| 90 | <CardContent className="p-6 text-sm text-muted-foreground"> | 95 | <CardContent className="p-6 text-sm text-muted-foreground"> |
| 91 | Select an organization from the switcher to manage members. | 96 | Select a workspace to manage members. |
| 92 | </CardContent> | 97 | </CardContent> |
| 93 | </Card> | 98 | </Card> |
| 94 | </div> | 99 | </div> |
| @@ -101,8 +106,8 @@ export function MembersView() { · 6 unmodified lines | |||
| 101 | <PageHeader title="Members" description="Manage organization membership and invites" /> | 106 | <PageHeader title="Members" description="Manage organization membership and invites" /> |
| 102 | <Card> | 107 | <Card> |
| 103 | <CardContent className="p-6 text-sm text-muted-foreground"> | 108 | <CardContent className="p-6 text-sm text-muted-foreground"> |
| 104 | Member management is waiting for an organization record for this workspace. The active | 109 | Member management is waiting for workspace provisioning to finish. This workspace is |
| 105 | workspace does not have a linked `organizationId` yet. | 110 | missing its internal organization link. |
| 106 | </CardContent> | 111 | </CardContent> |
| 107 | </Card> | 112 | </Card> |
| 108 | </div> | 113 | </div> |
| @@ -166,7 +171,7 @@ export function MembersView() { · 57 unmodified lines | |||
| 166 | <CardHeader> | 171 | <CardHeader> |
| 167 | <CardTitle className="text-sm flex items-center gap-2"> | 172 | <CardTitle className="text-sm flex items-center gap-2"> |
| 168 | <Users className="h-4 w-4" /> | 173 | <Users className="h-4 w-4" /> |
| 169 | Organization Members | 174 | Workspace Members |
| 170 | </CardTitle> | 175 | </CardTitle> |
| 171 | </CardHeader> | 176 | </CardHeader> |
| 172 | <CardContent className="space-y-3"> | 177 | <CardContent className="space-y-3"> |
| @@ -189,6 +194,9 @@ export function MembersView() { · 16 unmodified lines | |||
| 189 | onChange={async (event) => { | 194 | onChange={async (event) => { |
| 190 | setBusyMemberAccountId(member.accountId); | 195 | setBusyMemberAccountId(member.accountId); |
| 191 | try { | 196 | try { |
| 197 | if (!typedOrganizationId) { | ||
| 198 | return; | ||
| 199 | } | ||
| 192 | await updateRole({ | 200 | await updateRole({ |
| 193 | organizationId: typedOrganizationId, | 201 | organizationId: typedOrganizationId, |
| 194 | accountId: member.accountId, | 202 | accountId: member.accountId, |
| @@ -215,6 +223,9 @@ export function MembersView() { · 20 unmodified lines | |||
| 215 | onClick={async () => { | 223 | onClick={async () => { |
| 216 | setBusyMemberAccountId(member.accountId); | 224 | setBusyMemberAccountId(member.accountId); |
| 217 | try { | 225 | try { |
| 226 | if (!typedOrganizationId) { | ||
| 227 | return; | ||
| 228 | } | ||
| 218 | await updateBillable({ | 229 | await updateBillable({ |
| 219 | organizationId: typedOrganizationId, | 230 | organizationId: typedOrganizationId, |
| 220 | accountId: member.accountId, | 231 | accountId: member.accountId, |
| @@ -237,6 +248,9 @@ export function MembersView() { · 16 unmodified lines | |||
| 237 | onClick={async () => { | 248 | onClick={async () => { |
| 238 | setBusyMemberAccountId(member.accountId); | 249 | setBusyMemberAccountId(member.accountId); |
| 239 | try { | 250 | try { |
| 251 | if (!typedOrganizationId) { | ||
| 252 | return; | ||
| 253 | } | ||
| 240 | await removeMember({ | 254 | await removeMember({ |
| 241 | organizationId: typedOrganizationId, | 255 | organizationId: typedOrganizationId, |
| 242 | accountId: member.accountId, | 256 | accountId: member.accountId, |
| @@ -20,8 +20,8 @@ interface SessionState { · 19 unmodified lines | |||
| 20 | loading: boolean; | 20 | loading: boolean; |
| 21 | error: string | null; | 21 | error: string | null; |
| 22 | clientConfig: { | 22 | clientConfig: { |
| 23 | authProviderMode: "workos" | "local"; | 23 | authProviderMode: string; |
| 24 | invitesProvider: "workos" | "local"; | 24 | invitesProvider: string; |
| 25 | features: { | 25 | features: { |
| 26 | organizations: boolean; | 26 | organizations: boolean; |
| 27 | billing: boolean; | 27 | billing: boolean; |
| @@ -40,6 +40,7 @@ interface SessionState { · 12 unmodified lines | |||
| 40 | switchOrganization: (organizationId: string | null) => void; | 40 | switchOrganization: (organizationId: string | null) => void; |
| 41 | workspaces: Array<{ | 41 | workspaces: Array<{ |
| 42 | id: string; | 42 | id: string; |
| 43 | docId: Id<"workspaces"> | null; | ||
| 43 | name: string; | 44 | name: string; |
| 44 | kind: "organization" | "personal" | "anonymous"; | 45 | kind: "organization" | "personal" | "anonymous"; |
| 45 | organizationId: Id<"organizations"> | null; | 46 | organizationId: Id<"organizations"> | null; |
| @@ -236,7 +237,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 190 unmodified lines | |||
| 236 | useEffect(() => { | 237 | useEffect(() => { |
| 237 | if ( | 238 | if ( |
| 238 | !workosEnabled | 239 | !workosEnabled |
| 239 | || account !== null | 240 | || account === undefined |
| 240 | || bootstrappingWorkos | 241 | || bootstrappingWorkos |
| 241 | || workosBootstrapAttempted | 242 | || workosBootstrapAttempted |
| 242 | ) { | 243 | ) { |
| @@ -333,7 +334,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 90 unmodified lines | |||
| 333 | setCreatingWorkspace(true); | 334 | setCreatingWorkspace(true); |
| 334 | setError(null); | 335 | setError(null); |
| 335 | try { | 336 | try { |
| 336 | let iconStorageId: string | undefined; | 337 | let iconStorageId: Id<"_storage"> | undefined; |
| 337 | 338 | ||
| 338 | if (iconFile) { | 339 | if (iconFile) { |
| 339 | const uploadUrl = await generateWorkspaceIconUploadUrl({ | 340 | const uploadUrl = await generateWorkspaceIconUploadUrl({ |
| @@ -356,7 +357,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 16 unmodified lines | |||
| 356 | if (!json.storageId) { | 357 | if (!json.storageId) { |
| 357 | throw new Error("Upload did not return a storage id"); | 358 | throw new Error("Upload did not return a storage id"); |
| 358 | } | 359 | } |
| 359 | iconStorageId = json.storageId; | 360 | iconStorageId = json.storageId as Id<"_storage">; |
| 360 | } | 361 | } |
| 361 | 362 | ||
| 362 | const created = await createWorkspaceMutation({ | 363 | const created = await createWorkspaceMutation({ |
| @@ -423,8 +424,9 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 60 unmodified lines | |||
| 423 | ); | 424 | ); |
| 424 | const workspaceOptions = useMemo(() => { | 425 | const workspaceOptions = useMemo(() => { |
| 425 | if (mode === "workos" && workspaces) { | 426 | if (mode === "workos" && workspaces) { |
| 426 | return workspaces.map((workspace) => ({ | 427 | return workspaces.map((workspace): SessionState["workspaces"][number] => ({ |
| 427 | id: workspace.runtimeWorkspaceId, | 428 | id: workspace.runtimeWorkspaceId, |
| 429 | docId: workspace._id, | ||
| 428 | name: workspace.name, | 430 | name: workspace.name, |
| 429 | kind: | 431 | kind: |
| 430 | workspace.kind === "organization" || workspace.kind === "org" | 432 | workspace.kind === "organization" || workspace.kind === "org" |
| @@ -441,6 +443,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 10 unmodified lines | |||
| 441 | return [ | 443 | return [ |
| 442 | { | 444 | { |
| 443 | id: guestContext.workspaceId, | 445 | id: guestContext.workspaceId, |
| 446 | docId: null, | ||
| 444 | name: "Guest Workspace", | 447 | name: "Guest Workspace", |
| 445 | kind: "anonymous" as const, | 448 | kind: "anonymous" as const, |
| 446 | organizationId: null, | 449 | organizationId: null, |