History

Update dependencies and refactor routing in web application

- Added `react-router` dependency for improved routing capabilities. - Updated `@elysiajs/eden` to version 1.4.8 for enhanced functionality. - Implemented app shell rewrite exclusions in `next.config.ts` to streamline routing. - Replaced `AppShell` component with `StaticAppShell` in various pages for consistent layout handling. - Refactored navigation logic to utilize `react-router` hooks for better navigation management. - Removed unnecessary components and code from onboarding and organization pages to simplify structure.

Rhys Sullivan committed 9 months ago

Commit ad7cb931 parent b27ea0621 files · +400 −217Browse files

Unified · Split

21 files
  1. bun.lock +6 −1
  2. executor/apps/web/next.config.ts +21 −0
  3. executor/apps/web/package.json +1 −0
  4. executor/apps/web/src/app/approvals/page.tsx +2 −2
  5. executor/apps/web/src/app/layout.tsx +1 −2
  6. executor/apps/web/src/app/onboarding/page.tsx +2 −134
  7. executor/apps/web/src/app/organization/page.tsx +2 −7
  8. executor/apps/web/src/app/page.tsx +2 −2
  9. executor/apps/web/src/app/static-app-shell/page.tsx +5 −0
  10. executor/apps/web/src/app/tasks/page.tsx +2 −7
  11. executor/apps/web/src/app/tools/page.tsx +3 −14
  12. executor/apps/web/src/components/app-shell.tsx +86 −17
  13. executor/apps/web/src/components/approval-notifier.tsx +4 −4
  14. executor/apps/web/src/components/billing-view.tsx +2 −2
  15. executor/apps/web/src/components/dashboard-view.tsx +8 −8
  16. executor/apps/web/src/components/onboarding-view.tsx +136 −0
  17. executor/apps/web/src/components/organization-settings-view.tsx +6 −5
  18. executor/apps/web/src/components/static-app-shell.tsx +11 −0
  19. executor/apps/web/src/components/tasks-view.tsx +6 −6
  20. executor/apps/web/src/frontend/app.tsx +50 −0
  21. executor/apps/web/src/lib/session-context.tsx +44 −6
bun.lock+6−1
@@ -106,6 +106,7 @@ · 105 unmodified lines
106 "radix-ui": "^1.4.3",106 "radix-ui": "^1.4.3",
107 "react": "19.2.3",107 "react": "19.2.3",
108 "react-dom": "19.2.3",108 "react-dom": "19.2.3",
109 "react-router": "^7.9.5",
109 "shiki": "^3.22.0",110 "shiki": "^3.22.0",
110 "sonner": "^2.0.7",111 "sonner": "^2.0.7",
111 "streamdown": "^2.1.0",112 "streamdown": "^2.1.0",
@@ -317,7 +318,7 @@ · 205 unmodified lines
317 318
318 "@effect/rpc": ["@effect/rpc@0.72.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.93.3", "effect": "^3.19.5" } }, "sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ=="],319 "@effect/rpc": ["@effect/rpc@0.72.2", "", { "dependencies": { "msgpackr": "^1.11.4" }, "peerDependencies": { "@effect/platform": "^0.93.3", "effect": "^3.19.5" } }, "sha512-BmTXybXCOq96D2r9mvSW/YdiTQs5CStnd4II+lfVKrMr3pMNERKLZ2LG37Tfm4Sy3Q8ire6IVVKO/CN+VR0uQQ=="],
319 320
320 "@elysiajs/eden": ["@elysiajs/eden@1.4.6", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q=="],321 "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="],
321 322
322 "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],323 "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
323 324
@@ -2189,6 +2190,8 @@ · 1865 unmodified lines
2189 2190
2190 "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],2191 "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
2191 2192
2193 "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
2194
2192 "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],2195 "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
2193 2196
2194 "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],2197 "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
@@ -2269,6 +2272,8 @@ · 74 unmodified lines
2269 2272
2270 "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],2273 "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
2271 2274
2275 "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
2276
2272 "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],2277 "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
2273 2278
2274 "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],2279 "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
executor/apps/web/next.config.ts+21−0
@@ -1,4 +1,17 @@
1import type { NextConfig } from "next";1import type { NextConfig } from "next";
2
3const appShellRewriteExclusions = [
4 "api(?:/|$)",
5 "_next(?:/|$)",
6 "favicon\\.ico$",
7 "sign-in(?:/|$)",
8 "sign-up(?:/|$)",
9 "sign-out(?:/|$)",
10 "callback(?:/|$)",
11 "static-app-shell(?:/|$)",
12 ".*\\..*",
13].join("|");
14
2const nextConfig: NextConfig = {15const nextConfig: NextConfig = {
3 transpilePackages: ["@executor/contracts"],16 transpilePackages: ["@executor/contracts"],
4 env: {17 env: {
@@ -8,6 +21,14 @@ const nextConfig: NextConfig = { · 3 unmodified lines
8 NEXT_PUBLIC_WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID,21 NEXT_PUBLIC_WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID,
9 NEXT_PUBLIC_STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID,22 NEXT_PUBLIC_STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID,
10 },23 },
24 async rewrites() {
25 return [
26 {
27 source: `/((?!${appShellRewriteExclusions}).*)`,
28 destination: "/static-app-shell",
29 },
30 ];
31 },
11};32};
12 33
13export default nextConfig;34export default nextConfig;
executor/apps/web/package.json+1−0
@@ -28,6 +28,7 @@ · 27 unmodified lines
28 "radix-ui": "^1.4.3",28 "radix-ui": "^1.4.3",
29 "react": "19.2.3",29 "react": "19.2.3",
30 "react-dom": "19.2.3",30 "react-dom": "19.2.3",
31 "react-router": "^7.9.5",
31 "shiki": "^3.22.0",32 "shiki": "^3.22.0",
32 "sonner": "^2.0.7",33 "sonner": "^2.0.7",
33 "streamdown": "^2.1.0",34 "streamdown": "^2.1.0",
executor/apps/web/src/app/approvals/page.tsx+2−2
@@ -1,5 +1,5 @@
1import { ApprovalsView } from "@/components/approvals-view";1import { StaticAppShell } from "@/components/static-app-shell";
2 2
3export default function ApprovalsPage() {3export default function ApprovalsPage() {
4 return <ApprovalsView />;4 return <StaticAppShell />;
5}5}
executor/apps/web/src/app/layout.tsx+1−2
@@ -2,7 +2,6 @@ import type { Metadata } from "next"; · 1 unmodified line
2import { JetBrains_Mono, Inter } from "next/font/google";2import { JetBrains_Mono, Inter } from "next/font/google";
3import { ThemeProvider } from "next-themes";3import { ThemeProvider } from "next-themes";
4import { Toaster } from "@/components/ui/sonner";4import { Toaster } from "@/components/ui/sonner";
5import { AppShell } from "@/components/app-shell";
6import { AppConvexProvider } from "@/lib/convex-provider";5import { AppConvexProvider } from "@/lib/convex-provider";
7import { QueryProvider } from "@/lib/query-provider";6import { QueryProvider } from "@/lib/query-provider";
8import { SessionProvider } from "@/lib/session-context";7import { SessionProvider } from "@/lib/session-context";
@@ -42,7 +41,7 @@ export default function RootLayout({ · 33 unmodified lines
42 <AppConvexProvider>41 <AppConvexProvider>
43 <QueryProvider>42 <QueryProvider>
44 <SessionProvider>43 <SessionProvider>
45 <AppShell>{children}</AppShell>44 {children}
46 </SessionProvider>45 </SessionProvider>
47 </QueryProvider>46 </QueryProvider>
48 </AppConvexProvider>47 </AppConvexProvider>
executor/apps/web/src/app/onboarding/page.tsx+2−134
@@ -1,137 +1,5 @@
1"use client";1import { StaticAppShell } from "@/components/static-app-shell";
2
3import { useState, type FormEvent } from "react";
4import Link from "next/link";
5import { useRouter } from "next/navigation";
6import { useMutation } from "convex/react";
7import { Building2 } from "lucide-react";
8import { Button } from "@/components/ui/button";
9import {
10 Card,
11 CardContent,
12 CardDescription,
13 CardFooter,
14 CardHeader,
15 CardTitle,
16} from "@/components/ui/card";
17import { Input } from "@/components/ui/input";
18import { convexApi } from "@/lib/convex-api";
19import { useSession } from "@/lib/session-context";
20 2
21export default function OnboardingPage() {3export default function OnboardingPage() {
22 const router = useRouter();4 return <StaticAppShell />;
23 const createOrganization = useMutation(convexApi.organizations.create);
24 const {
25 isSignedInToWorkos,
26 organizations,
27 organizationsLoading,
28 switchWorkspace,
29 } = useSession();
30
31 const [name, setName] = useState("");
32 const [error, setError] = useState<string | null>(null);
33 const [creating, setCreating] = useState(false);
34
35 const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
36 event.preventDefault();
37 const trimmed = name.trim();
38 if (trimmed.length < 2) {
39 setError("Organization name must be at least 2 characters.");
40 return;
41 }
42
43 setCreating(true);
44 setError(null);
45 try {
46 const result = await createOrganization({ name: trimmed });
47 switchWorkspace(result.workspace.id);
48 router.replace("/");
49 } catch (cause) {
50 setError(cause instanceof Error ? cause.message : "Failed to create organization");
51 } finally {
52 setCreating(false);
53 }
54 };
55
56 if (!isSignedInToWorkos) {
57 return (
58 <div className="min-h-[70vh] grid place-items-center">
59 <Card className="w-full max-w-xl border-border">
60 <CardHeader>
61 <CardTitle>Sign in required</CardTitle>
62 <CardDescription>
63 You need to sign in before setting up an organization.
64 </CardDescription>
65 </CardHeader>
66 <CardFooter className="justify-between gap-2">
67 <Button asChild variant="outline">
68 <Link href="/">Back</Link>
69 </Button>
70 <Button asChild>
71 <Link href="/sign-in">Sign in</Link>
72 </Button>
73 </CardFooter>
74 </Card>
75 </div>
76 );
77 }
78
79 if (!organizationsLoading && organizations.length > 0) {
80 return (
81 <div className="min-h-[70vh] grid place-items-center">
82 <Card className="w-full max-w-xl border-border">
83 <CardHeader>
84 <CardTitle>Organization ready</CardTitle>
85 <CardDescription>
86 Your account is already linked to an organization.
87 </CardDescription>
88 </CardHeader>
89 <CardFooter>
90 <Button asChild className="w-full">
91 <Link href="/">Continue to dashboard</Link>
92 </Button>
93 </CardFooter>
94 </Card>
95 </div>
96 );
97 }
98
99 return (
100 <div className="min-h-[80vh] grid place-items-center">
101 <Card className="w-full max-w-xl border-border">
102 <CardHeader>
103 <div className="mb-2 inline-flex h-9 w-9 items-center justify-center rounded-md border border-border bg-muted">
104 <Building2 className="h-4 w-4 text-muted-foreground" />
105 </div>
106 <CardTitle>Name your organization</CardTitle>
107 <CardDescription>
108 This is your team space. You can invite members and create more workspaces later.
109 </CardDescription>
110 </CardHeader>
111 <form onSubmit={handleSubmit}>
112 <CardContent className="space-y-2">
113 <Input
114 value={name}
115 onChange={(event) => {
116 setError(null);
117 setName(event.target.value);
118 }}
119 placeholder="Acme"
120 maxLength={64}
121 autoFocus
122 />
123 {error ? <p className="text-xs text-destructive">{error}</p> : null}
124 </CardContent>
125 <CardFooter className="justify-end gap-2">
126 <Button asChild variant="outline" disabled={creating}>
127 <Link href="/sign-out">Sign out</Link>
128 </Button>
129 <Button type="submit" disabled={creating}>
130 {creating ? "Creating..." : "Continue"}
131 </Button>
132 </CardFooter>
133 </form>
134 </Card>
135 </div>
136 );
137}5}
executor/apps/web/src/app/organization/page.tsx+2−7
@@ -1,10 +1,5 @@
1import { Suspense } from "react";1import { StaticAppShell } from "@/components/static-app-shell";
2import { OrganizationSettingsView } from "@/components/organization-settings-view";
3 2
4export default function OrganizationPage() {3export default function OrganizationPage() {
5 return (4 return <StaticAppShell />;
6 <Suspense>
7 <OrganizationSettingsView />
8 </Suspense>
9 );
10}5}
executor/apps/web/src/app/page.tsx+2−2
@@ -1,5 +1,5 @@
1import { DashboardView } from "@/components/dashboard-view";1import { StaticAppShell } from "@/components/static-app-shell";
2 2
3export default function DashboardPage() {3export default function DashboardPage() {
4 return <DashboardView />;4 return <StaticAppShell />;
5}5}
executor/apps/web/src/app/static-app-shell/page.tsx+5−0
@@ -0,0 +1,5 @@
1import { StaticAppShell } from "@/components/static-app-shell";
2
3export default function StaticAppShellPage() {
4 return <StaticAppShell />;
5}
executor/apps/web/src/app/tasks/page.tsx+2−7
@@ -1,10 +1,5 @@
1import { Suspense } from "react";1import { StaticAppShell } from "@/components/static-app-shell";
2import { TasksView } from "@/components/tasks-view";
3 2
4export default function TasksPage() {3export default function TasksPage() {
5 return (4 return <StaticAppShell />;
6 <Suspense>
7 <TasksView />
8 </Suspense>
9 );
10}5}
executor/apps/web/src/app/tools/page.tsx+3−14
@@ -1,16 +1,5 @@
1import { ToolsView } from "@/components/tools-view";1import { StaticAppShell } from "@/components/static-app-shell";
2 2
3export default async function ToolsPage({3export default function ToolsPage() {
4 searchParams,4 return <StaticAppShell />;
5}: {
6 searchParams: Promise<Record<string, string | string[] | undefined>>;
7}) {
8 const params = await searchParams;
9 const source = typeof params.source === "string" ? params.source : null;
10
11 return (
12 <div className="h-full min-h-0">
13 <ToolsView initialSource={source} />
14 </div>
15 );
16}5}
executor/apps/web/src/components/app-shell.tsx+86−17
@@ -1,8 +1,7 @@
1"use client";1"use client";
2 2
3import { Suspense, useEffect, useMemo, useState, type FormEvent } from "react";3import { Suspense, useEffect, useMemo, useState, type FormEvent } from "react";
4import Link from "next/link";4import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
5import { usePathname, useRouter, useSearchParams } from "next/navigation";
6import { useMutation } from "convex/react";5import { useMutation } from "convex/react";
7import {6import {
8 LayoutDashboard,7 LayoutDashboard,
@@ -38,7 +37,7 @@ import { · 29 unmodified lines
38import { Input } from "@/components/ui/input";37import { Input } from "@/components/ui/input";
39import { useSession } from "@/lib/session-context";38import { useSession } from "@/lib/session-context";
40import { convexApi } from "@/lib/convex-api";39import { convexApi } from "@/lib/convex-api";
41import { workosEnabled } from "@/lib/auth-capabilities";40import { anonymousDemoEnabled, workosEnabled } from "@/lib/auth-capabilities";
42import { ThemeSwitcher } from "@/components/theme-switcher";41import { ThemeSwitcher } from "@/components/theme-switcher";
43import { ApprovalNotifier } from "@/components/approval-notifier";42import { ApprovalNotifier } from "@/components/approval-notifier";
44import { cn } from "@/lib/utils";43import { cn } from "@/lib/utils";
@@ -51,7 +50,8 @@ const NAV_ITEMS = [ · 6 unmodified lines
51];50];
52 51
53function NavLinks({ onClick }: { onClick?: () => void }) {52function NavLinks({ onClick }: { onClick?: () => void }) {
54 const pathname = usePathname();53 const location = useLocation();
54 const pathname = location.pathname;
55 55
56 return (56 return (
57 <nav className="flex flex-col gap-1">57 <nav className="flex flex-col gap-1">
@@ -63,7 +63,7 @@ function NavLinks({ onClick }: { onClick?: () => void }) { · 5 unmodified lines
63 return (63 return (
64 <Link64 <Link
65 key={item.href}65 key={item.href}
66 href={item.href}66 to={item.href}
67 onClick={onClick}67 onClick={onClick}
68 className={cn(68 className={cn(
69 "flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors",69 "flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors",
@@ -82,12 +82,11 @@ function NavLinks({ onClick }: { onClick?: () => void }) { · 12 unmodified lines
82}82}
83 83
84function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) {84function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) {
85 const router = useRouter();85 const navigate = useNavigate();
86 const createOrganizationMutation = useMutation(convexApi.organizations.create);86 const createOrganizationMutation = useMutation(convexApi.organizations.create);
87 const {87 const {
88 context,88 context,
89 mode,89 mode,
90 clientConfig,
91 workspaces,90 workspaces,
92 switchWorkspace,91 switchWorkspace,
93 creatingWorkspace,92 creatingWorkspace,
@@ -261,7 +260,7 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 167 unmodified lines
261 className="inline-flex h-5 w-5 items-center justify-center rounded-sm border border-border text-muted-foreground hover:bg-accent hover:text-foreground"260 className="inline-flex h-5 w-5 items-center justify-center rounded-sm border border-border text-muted-foreground hover:bg-accent hover:text-foreground"
262 onClick={() => {261 onClick={() => {
263 switchWorkspace(settingsWorkspace.id);262 switchWorkspace(settingsWorkspace.id);
264 router.push("/organization?tab=members");263 navigate("/organization?tab=members");
265 }}264 }}
266 aria-label={`Open ${group.organizationName} settings`}265 aria-label={`Open ${group.organizationName} settings`}
267 title="Organization settings"266 title="Organization settings"
@@ -402,14 +401,13 @@ function WorkspaceSelector({ inHeader = false }: { inHeader?: boolean }) { · 134 unmodified lines
402function SessionInfo() {401function SessionInfo() {
403 const {402 const {
404 loading,403 loading,
405 clientConfig,
406 isSignedInToWorkos,404 isSignedInToWorkos,
407 workosProfile,405 workosProfile,
408 context,406 context,
409 workspaces,407 workspaces,
410 resetWorkspace,408 resetWorkspace,
411 } = useSession();409 } = useSession();
412 const searchParams = useSearchParams();410 const [searchParams] = useSearchParams();
413 const deleteCurrentAccountMutation = useMutation(convexApi.accounts.deleteCurrentAccount);411 const deleteCurrentAccountMutation = useMutation(convexApi.accounts.deleteCurrentAccount);
414 const [accountSettingsOpen, setAccountSettingsOpen] = useState(false);412 const [accountSettingsOpen, setAccountSettingsOpen] = useState(false);
415 const [deleteConfirmText, setDeleteConfirmText] = useState("");413 const [deleteConfirmText, setDeleteConfirmText] = useState("");
@@ -523,7 +521,7 @@ function SessionInfo() { · 107 unmodified lines
523 Account Settings521 Account Settings
524 </DropdownMenuItem>522 </DropdownMenuItem>
525 <DropdownMenuItem asChild className="text-xs">523 <DropdownMenuItem asChild className="text-xs">
526 <Link href="/sign-out">Sign out</Link>524 <Link to="/sign-out" reloadDocument>Sign out</Link>
527 </DropdownMenuItem>525 </DropdownMenuItem>
528 </DropdownMenuContent>526 </DropdownMenuContent>
529 </DropdownMenu>527 </DropdownMenu>
@@ -587,7 +585,7 @@ function SessionInfo() { · 57 unmodified lines
587 ) : (585 ) : (
588 <div className="px-3 py-3 space-y-2">586 <div className="px-3 py-3 space-y-2">
589 {workosEnabled ? (587 {workosEnabled ? (
590 <Link href={signInHref} className="block">588 <Link to={signInHref} reloadDocument className="block">
591 <Button589 <Button
592 variant="outline"590 variant="outline"
593 className="w-full h-9 justify-center gap-2 text-xs font-medium"591 className="w-full h-9 justify-center gap-2 text-xs font-medium"
@@ -663,9 +661,79 @@ function MobileHeader() { · 69 unmodified lines
663 );661 );
664}662}
665 663
664function NoOrganizationModal({ enabled }: { enabled: boolean }) {
665 const {
666 loading,
667 organizations,
668 organizationsLoading,
669 context,
670 isSignedInToWorkos,
671 createAnonymousWorkspace,
672 creatingAnonymousWorkspace,
673 } = useSession();
674 const [error, setError] = useState<string | null>(null);
675
676 const shouldShow = enabled
677 && !loading
678 && !organizationsLoading
679 && !context
680 && !isSignedInToWorkos
681 && organizations.length === 0;
682
683 const handleCreateAnonymousWorkspace = async () => {
684 setError(null);
685 try {
686 await createAnonymousWorkspace();
687 } catch (cause) {
688 const message = cause instanceof Error ? cause.message : "Failed to create anonymous workspace";
689 setError(message);
690 }
691 };
692
693 return (
694 <Dialog open={shouldShow}>
695 <DialogContent
696 showCloseButton={false}
697 onEscapeKeyDown={(event) => event.preventDefault()}
698 onInteractOutside={(event) => event.preventDefault()}
699 >
700 <DialogHeader>
701 <DialogTitle>Choose how to continue</DialogTitle>
702 <DialogDescription>
703 Sign in to access your organizations, or create an anonymous workspace to continue as a guest.
704 </DialogDescription>
705 </DialogHeader>
706 <div className="space-y-2">
707 {workosEnabled ? (
708 <Button asChild className="w-full" disabled={creatingAnonymousWorkspace}>
709 <Link to="/sign-in" reloadDocument className="gap-2">
710 <LogIn className="h-4 w-4" />
711 Sign in
712 </Link>
713 </Button>
714 ) : null}
715 <Button
716 variant="outline"
717 className="w-full"
718 onClick={handleCreateAnonymousWorkspace}
719 disabled={creatingAnonymousWorkspace || !anonymousDemoEnabled}
720 >
721 {creatingAnonymousWorkspace ? "Creating anonymous workspace..." : "Create anonymous workspace"}
722 </Button>
723 {!anonymousDemoEnabled ? (
724 <p className="text-xs text-muted-foreground">Anonymous workspace creation is disabled.</p>
725 ) : null}
726 {error ? <p className="text-xs text-destructive">{error}</p> : null}
727 </div>
728 </DialogContent>
729 </Dialog>
730 );
731}
732
666export function AppShell({ children }: { children: React.ReactNode }) {733export function AppShell({ children }: { children: React.ReactNode }) {
667 const pathname = usePathname();734 const location = useLocation();
668 const router = useRouter();735 const pathname = location.pathname;
736 const navigate = useNavigate();
669 const { loading, organizations, organizationsLoading, isSignedInToWorkos } = useSession();737 const { loading, organizations, organizationsLoading, isSignedInToWorkos } = useSession();
670 738
671 const onOnboardingRoute = pathname.startsWith("/onboarding");739 const onOnboardingRoute = pathname.startsWith("/onboarding");
@@ -677,12 +745,12 @@ export function AppShell({ children }: { children: React.ReactNode }) { · 5 unmodified lines
677 }745 }
678 746
679 if (needsOnboarding && !onOnboardingRoute) {747 if (needsOnboarding && !onOnboardingRoute) {
680 router.replace("/onboarding");748 navigate("/onboarding", { replace: true });
681 return;749 return;
682 }750 }
683 751
684 if (!needsOnboarding && onOnboardingRoute && organizations.length > 0) {752 if (!needsOnboarding && onOnboardingRoute && organizations.length > 0) {
685 router.replace("/");753 navigate("/", { replace: true });
686 }754 }
687 }, [755 }, [
688 loading,756 loading,
@@ -690,7 +758,7 @@ export function AppShell({ children }: { children: React.ReactNode }) { · 1 unmodified line
690 needsOnboarding,758 needsOnboarding,
691 onOnboardingRoute,759 onOnboardingRoute,
692 organizations.length,760 organizations.length,
693 router,761 navigate,
694 ]);762 ]);
695 763
696 if (isSignedInToWorkos && organizationsLoading && !onOnboardingRoute) {764 if (isSignedInToWorkos && organizationsLoading && !onOnboardingRoute) {
@@ -712,6 +780,7 @@ export function AppShell({ children }: { children: React.ReactNode }) { · 15 unmodified lines
712 return (780 return (
713 <div className="flex min-h-screen">781 <div className="flex min-h-screen">
714 <ApprovalNotifier />782 <ApprovalNotifier />
783 <NoOrganizationModal enabled />
715 <Sidebar />784 <Sidebar />
716 <div className="flex-1 flex flex-col min-w-0">785 <div className="flex-1 flex flex-col min-w-0">
717 <MobileHeader />786 <MobileHeader />
executor/apps/web/src/components/approval-notifier.tsx+4−4
@@ -1,7 +1,7 @@
1"use client";1"use client";
2 2
3import { useEffect, useRef } from "react";3import { useEffect, useRef } from "react";
4import { useRouter } from "next/navigation";4import { useNavigate } from "react-router";
5import { useQuery } from "convex/react";5import { useQuery } from "convex/react";
6import { toast } from "sonner";6import { toast } from "sonner";
7import { useSession } from "@/lib/session-context";7import { useSession } from "@/lib/session-context";
@@ -9,7 +9,7 @@ import { convexApi } from "@/lib/convex-api"; · 1 unmodified line
9import type { PendingApprovalRecord } from "@/lib/types";9import type { PendingApprovalRecord } from "@/lib/types";
10 10
11export function ApprovalNotifier() {11export function ApprovalNotifier() {
12 const router = useRouter();12 const navigate = useNavigate();
13 const { context } = useSession();13 const { context } = useSession();
14 const seenApprovalIdsRef = useRef<Set<string>>(new Set());14 const seenApprovalIdsRef = useRef<Set<string>>(new Set());
15 const activeWorkspaceRef = useRef<string | null>(null);15 const activeWorkspaceRef = useRef<string | null>(null);
@@ -58,12 +58,12 @@ export function ApprovalNotifier() { · 42 unmodified lines
58 : "Open approvals to review pending tool calls.",58 : "Open approvals to review pending tool calls.",
59 action: {59 action: {
60 label: "Review",60 label: "Review",
61 onClick: () => router.push("/approvals"),61 onClick: () => navigate("/approvals"),
62 },62 },
63 },63 },
64 );64 );
65 65
66 }, [approvals, context, router]);66 }, [approvals, context, navigate]);
67 67
68 return null;68 return null;
69}69}
executor/apps/web/src/components/billing-view.tsx+2−2
@@ -1,7 +1,7 @@
1"use client";1"use client";
2 2
3import { useMemo, useState } from "react";3import { useMemo, useState } from "react";
4import { useSearchParams } from "next/navigation";4import { useSearchParams } from "react-router";
5import { useAction, useMutation, useQuery } from "convex/react";5import { useAction, useMutation, useQuery } from "convex/react";
6import { AlertTriangle, BadgeCheck, CreditCard, RefreshCcw } from "lucide-react";6import { AlertTriangle, BadgeCheck, CreditCard, RefreshCcw } from "lucide-react";
7import { PageHeader } from "@/components/page-header";7import { PageHeader } from "@/components/page-header";
@@ -50,7 +50,7 @@ function formatTimestamp(value: number | null | undefined): string { · 42 unmodified lines
50 50
51export function BillingView({ showHeader = true }: BillingViewProps) {51export function BillingView({ showHeader = true }: BillingViewProps) {
52 const { context, organizations, organizationsLoading, workspaces } = useSession();52 const { context, organizations, organizationsLoading, workspaces } = useSession();
53 const searchParams = useSearchParams();53 const [searchParams] = useSearchParams();
54 const [actionState, setActionState] = useState<"idle" | "running" | "success" | "error">("idle");54 const [actionState, setActionState] = useState<"idle" | "running" | "success" | "error">("idle");
55 const [actionMessage, setActionMessage] = useState<string | null>(null);55 const [actionMessage, setActionMessage] = useState<string | null>(null);
56 const [priceId, setPriceId] = useState(() => process.env.NEXT_PUBLIC_STRIPE_PRICE_ID ?? "");56 const [priceId, setPriceId] = useState(() => process.env.NEXT_PUBLIC_STRIPE_PRICE_ID ?? "");
executor/apps/web/src/components/dashboard-view.tsx+8−8
@@ -1,7 +1,7 @@
1"use client";1"use client";
2 2
3import { useMemo } from "react";3import { useMemo } from "react";
4import { useRouter } from "next/navigation";4import { useNavigate } from "react-router";
5import {5import {
6 Play,6 Play,
7 ShieldCheck,7 ShieldCheck,
@@ -67,10 +67,10 @@ function StatCard({ · 59 unmodified lines
67}67}
68 68
69function PendingApprovalRow({ approval }: { approval: PendingApprovalRecord }) {69function PendingApprovalRow({ approval }: { approval: PendingApprovalRecord }) {
70 const router = useRouter();70 const navigate = useNavigate();
71 return (71 return (
72 <button72 <button
73 onClick={() => router.push("/approvals")}73 onClick={() => navigate("/approvals")}
74 className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-accent/50 transition-colors text-left group"74 className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-accent/50 transition-colors text-left group"
75 >75 >
76 <div className="h-2 w-2 rounded-full bg-terminal-amber pulse-dot shrink-0" />76 <div className="h-2 w-2 rounded-full bg-terminal-amber pulse-dot shrink-0" />
@@ -88,10 +88,10 @@ function PendingApprovalRow({ approval }: { approval: PendingApprovalRecord }) { · 11 unmodified lines
88}88}
89 89
90function RecentTaskRow({ task }: { task: TaskRecord }) {90function RecentTaskRow({ task }: { task: TaskRecord }) {
91 const router = useRouter();91 const navigate = useNavigate();
92 return (92 return (
93 <button93 <button
94 onClick={() => router.push(`/tasks?selected=${task.id}`)}94 onClick={() => navigate(`/tasks?selected=${task.id}`)}
95 className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-accent/50 transition-colors text-left group"95 className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md hover:bg-accent/50 transition-colors text-left group"
96 >96 >
97 <div className="flex-1 min-w-0">97 <div className="flex-1 min-w-0">
@@ -122,7 +122,7 @@ function sourceType(source?: string): string { · 24 unmodified lines
122}122}
123 123
124function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) {124function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) {
125 const router = useRouter();125 const navigate = useNavigate();
126 126
127 const groups = useMemo(() => {127 const groups = useMemo(() => {
128 const map = new Map<128 const map = new Map<
@@ -180,7 +180,7 @@ function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { · 51 unmodified lines
180 variant="ghost"180 variant="ghost"
181 size="sm"181 size="sm"
182 className="text-xs h-7"182 className="text-xs h-7"
183 onClick={() => router.push("/tools")}183 onClick={() => navigate("/tools")}
184 >184 >
185 Manage185 Manage
186 <ArrowRight className="h-3 w-3 ml-1" />186 <ArrowRight className="h-3 w-3 ml-1" />
@@ -194,7 +194,7 @@ function ToolsSummaryCard({ tools }: { tools: ToolDescriptor[] }) { · 7 unmodified lines
194 return (194 return (
195 <button195 <button
196 key={group.name}196 key={group.name}
197 onClick={() => router.push(`/tools?source=${encodeURIComponent(group.name)}`)}197 onClick={() => navigate(`/tools?source=${encodeURIComponent(group.name)}`)}
198 className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent/40 transition-colors text-left group/row w-full"198 className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent/40 transition-colors text-left group/row w-full"
199 >199 >
200 <div className="h-7 w-7 rounded bg-muted flex items-center justify-center shrink-0">200 <div className="h-7 w-7 rounded bg-muted flex items-center justify-center shrink-0">
executor/apps/web/src/components/onboarding-view.tsx+136−0
@@ -0,0 +1,136 @@
1"use client";
2
3import { useState, type FormEvent } from "react";
4import { Link, useNavigate } from "react-router";
5import { useMutation } from "convex/react";
6import { Building2 } from "lucide-react";
7import { Button } from "@/components/ui/button";
8import {
9 Card,
10 CardContent,
11 CardDescription,
12 CardFooter,
13 CardHeader,
14 CardTitle,
15} from "@/components/ui/card";
16import { Input } from "@/components/ui/input";
17import { convexApi } from "@/lib/convex-api";
18import { useSession } from "@/lib/session-context";
19
20export function OnboardingView() {
21 const navigate = useNavigate();
22 const createOrganization = useMutation(convexApi.organizations.create);
23 const {
24 isSignedInToWorkos,
25 organizations,
26 organizationsLoading,
27 switchWorkspace,
28 } = useSession();
29
30 const [name, setName] = useState("");
31 const [error, setError] = useState<string | null>(null);
32 const [creating, setCreating] = useState(false);
33
34 const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
35 event.preventDefault();
36 const trimmed = name.trim();
37 if (trimmed.length < 2) {
38 setError("Organization name must be at least 2 characters.");
39 return;
40 }
41
42 setCreating(true);
43 setError(null);
44 try {
45 const result = await createOrganization({ name: trimmed });
46 switchWorkspace(result.workspace.id);
47 navigate("/", { replace: true });
48 } catch (cause) {
49 setError(cause instanceof Error ? cause.message : "Failed to create organization");
50 } finally {
51 setCreating(false);
52 }
53 };
54
55 if (!isSignedInToWorkos) {
56 return (
57 <div className="min-h-[70vh] grid place-items-center">
58 <Card className="w-full max-w-xl border-border">
59 <CardHeader>
60 <CardTitle>Sign in required</CardTitle>
61 <CardDescription>
62 You need to sign in before setting up an organization.
63 </CardDescription>
64 </CardHeader>
65 <CardFooter className="justify-between gap-2">
66 <Button asChild variant="outline">
67 <Link to="/">Back</Link>
68 </Button>
69 <Button asChild>
70 <Link to="/sign-in" reloadDocument>Sign in</Link>
71 </Button>
72 </CardFooter>
73 </Card>
74 </div>
75 );
76 }
77
78 if (!organizationsLoading && organizations.length > 0) {
79 return (
80 <div className="min-h-[70vh] grid place-items-center">
81 <Card className="w-full max-w-xl border-border">
82 <CardHeader>
83 <CardTitle>Organization ready</CardTitle>
84 <CardDescription>
85 Your account is already linked to an organization.
86 </CardDescription>
87 </CardHeader>
88 <CardFooter>
89 <Button asChild className="w-full">
90 <Link to="/">Continue to dashboard</Link>
91 </Button>
92 </CardFooter>
93 </Card>
94 </div>
95 );
96 }
97
98 return (
99 <div className="min-h-[80vh] grid place-items-center">
100 <Card className="w-full max-w-xl border-border">
101 <CardHeader>
102 <div className="mb-2 inline-flex h-9 w-9 items-center justify-center rounded-md border border-border bg-muted">
103 <Building2 className="h-4 w-4 text-muted-foreground" />
104 </div>
105 <CardTitle>Name your organization</CardTitle>
106 <CardDescription>
107 This is your team space. You can invite members and create more workspaces later.
108 </CardDescription>
109 </CardHeader>
110 <form onSubmit={handleSubmit}>
111 <CardContent className="space-y-2">
112 <Input
113 value={name}
114 onChange={(event) => {
115 setError(null);
116 setName(event.target.value);
117 }}
118 placeholder="Acme"
119 maxLength={64}
120 autoFocus
121 />
122 {error ? <p className="text-xs text-destructive">{error}</p> : null}
123 </CardContent>
124 <CardFooter className="justify-end gap-2">
125 <Button asChild variant="outline" disabled={creating}>
126 <Link to="/sign-out" reloadDocument>Sign out</Link>
127 </Button>
128 <Button type="submit" disabled={creating}>
129 {creating ? "Creating..." : "Continue"}
130 </Button>
131 </CardFooter>
132 </form>
133 </Card>
134 </div>
135 );
136}
executor/apps/web/src/components/organization-settings-view.tsx+6−5
@@ -1,7 +1,7 @@
1"use client";1"use client";
2 2
3import { useMemo } from "react";3import { useMemo } from "react";
4import { usePathname, useRouter, useSearchParams } from "next/navigation";4import { useLocation, useNavigate, useSearchParams } from "react-router";
5import { CreditCard, Users } from "lucide-react";5import { CreditCard, Users } from "lucide-react";
6import { BillingView } from "@/components/billing-view";6import { BillingView } from "@/components/billing-view";
7import { MembersView } from "@/components/members-view";7import { MembersView } from "@/components/members-view";
@@ -15,9 +15,10 @@ function normalizeTab(value: string | null): OrganizationTab { · 7 unmodified lines
15}15}
16 16
17export function OrganizationSettingsView() {17export function OrganizationSettingsView() {
18 const router = useRouter();18 const navigate = useNavigate();
19 const pathname = usePathname();19 const location = useLocation();
20 const searchParams = useSearchParams();20 const pathname = location.pathname;
21 const [searchParams] = useSearchParams();
21 22
22 const tab = useMemo(() => normalizeTab(searchParams.get("tab")), [searchParams]);23 const tab = useMemo(() => normalizeTab(searchParams.get("tab")), [searchParams]);
23 24
@@ -25,7 +26,7 @@ export function OrganizationSettingsView() { · 1 unmodified line
25 const normalizedTab = normalizeTab(nextTab);26 const normalizedTab = normalizeTab(nextTab);
26 const nextSearch = new URLSearchParams(searchParams.toString());27 const nextSearch = new URLSearchParams(searchParams.toString());
27 nextSearch.set("tab", normalizedTab);28 nextSearch.set("tab", normalizedTab);
28 router.replace(`${pathname}?${nextSearch.toString()}`);29 navigate(`${pathname}?${nextSearch.toString()}`, { replace: true });
29 };30 };
30 31
31 return (32 return (
executor/apps/web/src/components/static-app-shell.tsx+11−0
@@ -0,0 +1,11 @@
1"use client";
2
3import dynamic from "next/dynamic";
4
5const FrontendApp = dynamic(() => import("@/frontend/app"), {
6 ssr: false,
7});
8
9export function StaticAppShell() {
10 return <FrontendApp />;
11}
executor/apps/web/src/components/tasks-view.tsx+6−6
@@ -1,7 +1,7 @@
1"use client";1"use client";
2 2
3import { useState, useCallback, useMemo } from "react";3import { useState, useCallback, useMemo } from "react";
4import { useSearchParams, useRouter } from "next/navigation";4import { useNavigate, useSearchParams } from "react-router";
5import {5import {
6 Play,6 Play,
7 ChevronRight,7 ChevronRight,
@@ -361,8 +361,8 @@ function TaskDetail({ · 353 unmodified lines
361 361
362export function TasksView() {362export function TasksView() {
363 const { context, loading: sessionLoading } = useSession();363 const { context, loading: sessionLoading } = useSession();
364 const router = useRouter();364 const navigate = useNavigate();
365 const searchParams = useSearchParams();365 const [searchParams] = useSearchParams();
366 const selectedId = searchParams.get("selected");366 const selectedId = searchParams.get("selected");
367 367
368 const tasks = useQuery(368 const tasks = useQuery(
@@ -377,12 +377,12 @@ export function TasksView() { · 8 unmodified lines
377 const selectTask = useCallback(377 const selectTask = useCallback(
378 (taskId: string | null) => {378 (taskId: string | null) => {
379 if (taskId) {379 if (taskId) {
380 router.push(`/tasks?selected=${taskId}`, { scroll: false });380 navigate(`/tasks?selected=${taskId}`);
381 } else {381 } else {
382 router.push("/tasks", { scroll: false });382 navigate("/tasks");
383 }383 }
384 },384 },
385 [router],385 [navigate],
386 );386 );
387 387
388 if (sessionLoading) {388 if (sessionLoading) {
executor/apps/web/src/frontend/app.tsx+50−0
@@ -0,0 +1,50 @@
1"use client";
2
3import { Navigate, Outlet, Route, Routes, BrowserRouter, useSearchParams } from "react-router";
4import { AppShell } from "@/components/app-shell";
5import { ApprovalsView } from "@/components/approvals-view";
6import { DashboardView } from "@/components/dashboard-view";
7import { OnboardingView } from "@/components/onboarding-view";
8import { OrganizationSettingsView } from "@/components/organization-settings-view";
9import { TasksView } from "@/components/tasks-view";
10import { ToolsView } from "@/components/tools-view";
11
12function ToolsRoute() {
13 const [searchParams] = useSearchParams();
14 const source = searchParams.get("source");
15
16 return (
17 <div className="h-full min-h-0">
18 <ToolsView initialSource={source} />
19 </div>
20 );
21}
22
23function ShellLayout() {
24 return (
25 <AppShell>
26 <Outlet />
27 </AppShell>
28 );
29}
30
31export default function FrontendApp() {
32 return (
33 <BrowserRouter>
34 <Routes>
35 <Route element={<ShellLayout />}>
36 <Route path="/" element={<DashboardView />} />
37 <Route path="/static-app-shell" element={<DashboardView />} />
38 <Route path="/tasks" element={<TasksView />} />
39 <Route path="/approvals" element={<ApprovalsView />} />
40 <Route path="/tools" element={<ToolsRoute />} />
41 <Route path="/organization" element={<OrganizationSettingsView />} />
42 <Route path="/onboarding" element={<OnboardingView />} />
43 <Route path="/members" element={<Navigate to="/organization?tab=members" replace />} />
44 <Route path="/billing" element={<Navigate to="/organization?tab=billing" replace />} />
45 <Route path="*" element={<Navigate to="/" replace />} />
46 </Route>
47 </Routes>
48 </BrowserRouter>
49 );
50}
executor/apps/web/src/lib/session-context.tsx+44−6
@@ -10,7 +10,7 @@ import { · 9 unmodified lines
10} from "react";10} from "react";
11import { useMutation, useQuery as useConvexQuery } from "convex/react";11import { useMutation, useQuery as useConvexQuery } from "convex/react";
12import { useQuery as useTanstackQuery } from "@tanstack/react-query";12import { useQuery as useTanstackQuery } from "@tanstack/react-query";
13import { workosEnabled } from "@/lib/auth-capabilities";13import { anonymousDemoEnabled, workosEnabled } from "@/lib/auth-capabilities";
14import { useWorkosAuthLoading } from "@/lib/convex-provider";14import { useWorkosAuthLoading } from "@/lib/convex-provider";
15import { convexApi } from "@/lib/convex-api";15import { convexApi } from "@/lib/convex-api";
16import type { AnonymousContext } from "./types";16import type { AnonymousContext } from "./types";
@@ -54,6 +54,8 @@ interface SessionState { · 37 unmodified lines
54 iconFile?: File | null,54 iconFile?: File | null,
55 organizationId?: Id<"organizations">,55 organizationId?: Id<"organizations">,
56 ) => Promise<void>;56 ) => Promise<void>;
57 creatingAnonymousWorkspace: boolean;
58 createAnonymousWorkspace: () => Promise<void>;
57 isSignedInToWorkos: boolean;59 isSignedInToWorkos: boolean;
58 workosProfile: {60 workosProfile: {
59 name: string;61 name: string;
@@ -95,6 +97,8 @@ const SessionContext = createContext<SessionState>({ · 35 unmodified lines
95 switchWorkspace: () => {},97 switchWorkspace: () => {},
96 creatingWorkspace: false,98 creatingWorkspace: false,
97 createWorkspace: async () => {},99 createWorkspace: async () => {},
100 creatingAnonymousWorkspace: false,
101 createAnonymousWorkspace: async () => {},
98 isSignedInToWorkos: false,102 isSignedInToWorkos: false,
99 workosProfile: null,103 workosProfile: null,
100 resetWorkspace: async () => {},104 resetWorkspace: async () => {},
@@ -143,13 +147,20 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 42 unmodified lines
143 const createWorkspaceMutation = useMutation(convexApi.workspaces.create);147 const createWorkspaceMutation = useMutation(convexApi.workspaces.create);
144 const generateWorkspaceIconUploadUrl = useMutation(convexApi.workspaces.generateWorkspaceIconUploadUrl);148 const generateWorkspaceIconUploadUrl = useMutation(convexApi.workspaces.generateWorkspaceIconUploadUrl);
145 const [creatingWorkspace, setCreatingWorkspace] = useState(false);149 const [creatingWorkspace, setCreatingWorkspace] = useState(false);
150 const [creatingAnonymousWorkspace, setCreatingAnonymousWorkspace] = useState(false);
151 const [manualGuestContext, setManualGuestContext] = useState<AnonymousContext | null>(null);
146 152
147 const bootstrapSessionQuery = useTanstackQuery({153 const bootstrapSessionQuery = useTanstackQuery({
148 queryKey: ["session-bootstrap", storedSessionId ?? "new"],154 queryKey: ["session-bootstrap", storedSessionId ?? "new"],
155 enabled: storedSessionId !== null,
149 retry: false,156 retry: false,
150 staleTime: Number.POSITIVE_INFINITY,157 staleTime: Number.POSITIVE_INFINITY,
151 queryFn: async () => {158 queryFn: async () => {
152 const context = await bootstrapAnonymousSession({ sessionId: storedSessionId ?? undefined });159 if (!storedSessionId) {
160 throw new Error("No guest session id available");
161 }
162
163 const context = await bootstrapAnonymousSession({ sessionId: storedSessionId });
153 localStorage.setItem(SESSION_KEY, context.sessionId);164 localStorage.setItem(SESSION_KEY, context.sessionId);
154 if (context.sessionId !== storedSessionId) {165 if (context.sessionId !== storedSessionId) {
155 setStoredSessionId(context.sessionId);166 setStoredSessionId(context.sessionId);
@@ -158,7 +169,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 2 unmodified lines
158 },169 },
159 });170 });
160 171
161 const guestContext: AnonymousContext | null = bootstrapSessionQuery.data ?? null;172 const guestContext: AnonymousContext | null = manualGuestContext ?? bootstrapSessionQuery.data ?? null;
162 173
163 const account = useConvexQuery(174 const account = useConvexQuery(
164 convexApi.app.getCurrentAccount,175 convexApi.app.getCurrentAccount,
@@ -204,9 +215,33 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 39 unmodified lines
204 localStorage.removeItem(ACTIVE_WORKSPACE_KEY);215 localStorage.removeItem(ACTIVE_WORKSPACE_KEY);
205 setStoredSessionId(null);216 setStoredSessionId(null);
206 setActiveWorkspaceId(null);217 setActiveWorkspaceId(null);
218 setManualGuestContext(null);
207 setRuntimeError(null);219 setRuntimeError(null);
208 }, []);220 }, []);
209 221
222 const createAnonymousWorkspace = useCallback(async () => {
223 if (!anonymousDemoEnabled) {
224 throw new Error("Anonymous workspace creation is disabled");
225 }
226
227 setRuntimeError(null);
228 setCreatingAnonymousWorkspace(true);
229 try {
230 const context = await bootstrapAnonymousSession({});
231 localStorage.setItem(SESSION_KEY, context.sessionId);
232 localStorage.setItem(ACTIVE_WORKSPACE_KEY, context.workspaceId);
233 setStoredSessionId(context.sessionId);
234 setActiveWorkspaceId(context.workspaceId as Id<"workspaces">);
235 setManualGuestContext(context);
236 } catch (cause) {
237 const message = cause instanceof Error ? cause.message : "Failed to create anonymous workspace";
238 setRuntimeError(message);
239 throw cause;
240 } finally {
241 setCreatingAnonymousWorkspace(false);
242 }
243 }, [bootstrapAnonymousSession]);
244
210 const switchWorkspace = useCallback((workspaceId: Id<"workspaces">) => {245 const switchWorkspace = useCallback((workspaceId: Id<"workspaces">) => {
211 setActiveWorkspaceId(workspaceId);246 setActiveWorkspaceId(workspaceId);
212 localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId);247 localStorage.setItem(ACTIVE_WORKSPACE_KEY, workspaceId);
@@ -318,9 +353,9 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 105 unmodified lines
318 const context = workosContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext);353 const context = workosContext ?? ((workosStillLoading || shouldBlockGuestFallback) ? null : guestContext);
319 354
320 const bootstrapSessionError =355 const bootstrapSessionError =
321 bootstrapSessionQuery.error instanceof Error356 storedSessionId && bootstrapSessionQuery.error instanceof Error
322 ? bootstrapSessionQuery.error.message357 ? bootstrapSessionQuery.error.message
323 : bootstrapSessionQuery.error358 : storedSessionId && bootstrapSessionQuery.error
324 ? "Failed to bootstrap session"359 ? "Failed to bootstrap session"
325 : null;360 : null;
326 const bootstrapWorkosError =361 const bootstrapWorkosError =
@@ -332,7 +367,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 5 unmodified lines
332 const error = runtimeError ?? bootstrapSessionError ?? bootstrapWorkosError;367 const error = runtimeError ?? bootstrapSessionError ?? bootstrapWorkosError;
333 368
334 const effectiveLoading = !context && !error && (369 const effectiveLoading = !context && !error && (
335 bootstrapSessionQuery.isLoading370 creatingAnonymousWorkspace
371 || (storedSessionId !== null && bootstrapSessionQuery.isLoading)
336 || workosStillLoading372 || workosStillLoading
337 || bootstrapWorkosAccountQuery.isFetching373 || bootstrapWorkosAccountQuery.isFetching
338 );374 );
@@ -379,6 +415,8 @@ export function SessionProvider({ children }: { children: ReactNode }) { · 40 unmodified lines
379 switchWorkspace,415 switchWorkspace,
380 creatingWorkspace,416 creatingWorkspace,
381 createWorkspace,417 createWorkspace,
418 creatingAnonymousWorkspace,
419 createAnonymousWorkspace,
382 isSignedInToWorkos: Boolean(account && account.provider === "workos"),420 isSignedInToWorkos: Boolean(account && account.provider === "workos"),
383 workosProfile:421 workosProfile:
384 account && account.provider === "workos"422 account && account.provider === "workos"