History

Polish web workspace shell and member billing flows

Rhys Sullivan committed 9 months ago

Commit ffb327f1 parent 8e23be511 files · +90 −164Browse files

Unified · Split

11 files
  1. executor/apps/web/src/app/approvals/page.tsx +1 −6
  2. executor/apps/web/src/app/billing/page.tsx +1 −6
  3. executor/apps/web/src/app/layout.tsx +2 −1
  4. executor/apps/web/src/app/members/page.tsx +1 −6
  5. executor/apps/web/src/app/page.tsx +1 −6
  6. executor/apps/web/src/app/tasks/page.tsx +3 −6
  7. executor/apps/web/src/app/tools/page.tsx +1 −6
  8. executor/apps/web/src/components/app-shell.tsx +25 −89
  9. executor/apps/web/src/components/billing-view.tsx +6 −6
  10. executor/apps/web/src/components/members-view.tsx +40 −26
  11. executor/apps/web/src/lib/session-context.tsx +9 −6
executor/apps/web/src/app/approvals/page.tsx+1−6
@@ -1,10 +1,5 @@
1import { AppShell } from "@/components/app-shell";
2import { ApprovalsView } from "@/components/approvals-view";1import { ApprovalsView } from "@/components/approvals-view";
3 2
4export default function ApprovalsPage() {3export default function ApprovalsPage() {
5 return (4 return <ApprovalsView />;
6 <AppShell>
7 <ApprovalsView />
8 </AppShell>
9 );
10}5}
executor/apps/web/src/app/billing/page.tsx+1−6
@@ -1,10 +1,5 @@
1import { AppShell } from "@/components/app-shell";
2import { BillingView } from "@/components/billing-view";1import { BillingView } from "@/components/billing-view";
3 2
4export default function BillingPage() {3export default function BillingPage() {
5 return (4 return <BillingView />;
6 <AppShell>
7 <BillingView />
8 </AppShell>
9 );
10}5}
executor/apps/web/src/app/layout.tsx+2−1
@@ -1,6 +1,7 @@
1import type { Metadata } from "next";1import type { Metadata } from "next";
2import { JetBrains_Mono, Inter } from "next/font/google";2import { JetBrains_Mono, Inter } from "next/font/google";
3import { Toaster } from "sonner";3import { Toaster } from "sonner";
4import { AppShell } from "@/components/app-shell";
4import { AppConvexProvider } from "@/lib/convex-provider";5import { AppConvexProvider } from "@/lib/convex-provider";
5import { QueryProvider } from "@/lib/query-provider";6import { QueryProvider } from "@/lib/query-provider";
6import { SessionProvider } from "@/lib/session-context";7import { 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>
executor/apps/web/src/app/members/page.tsx+1−6
@@ -1,10 +1,5 @@
1import { AppShell } from "@/components/app-shell";
2import { MembersView } from "@/components/members-view";1import { MembersView } from "@/components/members-view";
3 2
4export default function MembersPage() {3export default function MembersPage() {
5 return (4 return <MembersView />;
6 <AppShell>
7 <MembersView />
8 </AppShell>
9 );
10}5}
executor/apps/web/src/app/page.tsx+1−6
@@ -1,10 +1,5 @@
1import { AppShell } from "@/components/app-shell";
2import { DashboardView } from "@/components/dashboard-view";1import { DashboardView } from "@/components/dashboard-view";
3 2
4export default function DashboardPage() {3export default function DashboardPage() {
5 return (4 return <DashboardView />;
6 <AppShell>
7 <DashboardView />
8 </AppShell>
9 );
10}5}
executor/apps/web/src/app/tasks/page.tsx+3−6
@@ -1,13 +1,10 @@
1import { Suspense } from "react";1import { Suspense } from "react";
2import { AppShell } from "@/components/app-shell";
3import { TasksView } from "@/components/tasks-view";2import { TasksView } from "@/components/tasks-view";
4 3
5export default function TasksPage() {4export 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}
executor/apps/web/src/app/tools/page.tsx+1−6
@@ -1,10 +1,5 @@
1import { AppShell } from "@/components/app-shell";
2import { ToolsView } from "@/components/tools-view";1import { ToolsView } from "@/components/tools-view";
3 2
4export default function ToolsPage() {3export default function ToolsPage() {
5 return (4 return <ToolsView />;
6 <AppShell>
7 <ToolsView />
8 </AppShell>
9 );
10}5}
executor/apps/web/src/components/app-shell.tsx+25−89
@@ -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 = context97 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 <DropdownMenuItem176 <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 <img183 {(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 workspaces193 No workspaces
258 </DropdownMenuItem>194 </DropdownMenuItem>
executor/apps/web/src/components/billing-view.tsx+6−6
@@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; · 7 unmodified lines
8import { useSession } from "@/lib/session-context";8import { useSession } from "@/lib/session-context";
9 9
10export function BillingView() {10export 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 = context18 const activeWorkspace = context
19 ? workspaces.find((workspace) => workspace.id === context.workspaceId) ?? null19 ? 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 workspace56 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>
executor/apps/web/src/components/members-view.tsx+40−26
@@ -1,6 +1,6 @@
1"use client";1"use client";
2 2
3import { useMemo, useState } from "react";3import { useState } from "react";
4import { useMutation, useQuery } from "convex/react";4import { useMutation, useQuery } from "convex/react";
5import { Mail, UserMinus, Users } from "lucide-react";5import { Mail, UserMinus, Users } from "lucide-react";
6import { PageHeader } from "@/components/page-header";6import { 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) ?? null28 ? 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 active109 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 Members174 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,
executor/apps/web/src/lib/session-context.tsx+9−6
@@ -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 !workosEnabled239 !workosEnabled
239 || account !== null240 || account === undefined
240 || bootstrappingWorkos241 || bootstrappingWorkos
241 || workosBootstrapAttempted242 || 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,