From a051ba0e28bd63ad37fe05968d6a258d5ddf5c1c Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:27:36 +0000 Subject: [PATCH] feat(web): give Configure sections real routes under /configure (#1378) Configure sections were local useState, so they could not be linked or bookmarked and always opened on the default section. Each section is now a route (/configure, /configure/models, /configure/workspace-prompt, /configure/proactivity, /configure/automations), mirroring the existing /integrations/[card] pattern. The shell renders from the segment layout so switching sections does not remount the app, and legacy /?view=configure links forward to /configure. Fixes ENG-1140 --- .../app/(app)/configure/[section]/page.tsx | 17 +++++++++ apps/web/app/(app)/configure/layout.tsx | 15 ++++++++ apps/web/app/(app)/configure/page.tsx | 4 ++ apps/web/components/configure-view.tsx | 33 +++++++++-------- apps/web/lib/configure-routes.ts | 37 +++++++++++++++++++ apps/web/lib/view-mode-context.tsx | 30 +++++++++++---- 6 files changed, 112 insertions(+), 24 deletions(-) create mode 100644 apps/web/app/(app)/configure/[section]/page.tsx create mode 100644 apps/web/app/(app)/configure/layout.tsx create mode 100644 apps/web/app/(app)/configure/page.tsx create mode 100644 apps/web/lib/configure-routes.ts diff --git a/apps/web/app/(app)/configure/[section]/page.tsx b/apps/web/app/(app)/configure/[section]/page.tsx new file mode 100644 index 00000000..5acd2b3a --- /dev/null +++ b/apps/web/app/(app)/configure/[section]/page.tsx @@ -0,0 +1,17 @@ +import { notFound, redirect } from "next/navigation" +import { + DEFAULT_CONFIGURE_SECTION, + isConfigureSection, +} from "@/lib/configure-routes" + +export default async function ConfigureSectionPage({ + params, +}: { + params: Promise<{ section: string }> +}) { + const { section } = await params + // Default section is canonical at /configure. + if (section === DEFAULT_CONFIGURE_SECTION) redirect("/configure") + if (!isConfigureSection(section)) notFound() + return null +} diff --git a/apps/web/app/(app)/configure/layout.tsx b/apps/web/app/(app)/configure/layout.tsx new file mode 100644 index 00000000..340bd4c1 --- /dev/null +++ b/apps/web/app/(app)/configure/layout.tsx @@ -0,0 +1,15 @@ +import { AppExperience } from "@/components/app-experience" + +// Shell lives here so section nav doesn't remount the app. +export default function ConfigureLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + <> + + {children} + + ) +} diff --git a/apps/web/app/(app)/configure/page.tsx b/apps/web/app/(app)/configure/page.tsx new file mode 100644 index 00000000..fe7bd566 --- /dev/null +++ b/apps/web/app/(app)/configure/page.tsx @@ -0,0 +1,4 @@ +// Shell renders in layout.tsx. +export default function ConfigurePage() { + return null +} diff --git a/apps/web/components/configure-view.tsx b/apps/web/components/configure-view.tsx index 21206920..98be696b 100644 --- a/apps/web/components/configure-view.tsx +++ b/apps/web/components/configure-view.tsx @@ -2,7 +2,8 @@ import { cn } from "@lib/utils" import { Blocks, CalendarClock, Cpu, ScrollText } from "lucide-react" -import { useState } from "react" +import Link from "next/link" +import { usePathname } from "next/navigation" import CompanyBrainConnections from "@/components/settings/company-brain-connections" import CompanyBrainModels from "@/components/settings/company-brain-models" import CompanyBrainProactivity from "@/components/settings/company-brain-proactivity" @@ -11,15 +12,14 @@ import { ProactivenessIcon } from "@/components/settings/proactiveness-icon" import { WorkspacePrompt } from "@/components/settings/workspace-prompt" import { ErrorBoundary } from "@/components/error-boundary" import { useAuth } from "@lib/auth-context" +import { + type ConfigureSection, + configureSectionToPath, + DEFAULT_CONFIGURE_SECTION, + pathToConfigureSection, +} from "@/lib/configure-routes" import { dmSans125ClassName } from "@/lib/fonts" -type ConfigureSection = - | "company-brain" - | "models" - | "workspace-prompt" - | "proactivity" - | "automations" - const SECTIONS: { id: ConfigureSection label: string @@ -27,7 +27,7 @@ const SECTIONS: { icon: React.ComponentType<{ className?: string }> }[] = [ { - id: "company-brain", + id: "tools", label: "Integrations", description: "Connect the tools your brain works with. Your account covers your own actions and reads; workspace accounts are a shared fallback.", @@ -65,8 +65,10 @@ const SECTIONS: { export function ConfigureView() { const { org } = useAuth() - const [activeSection, setActiveSection] = - useState("company-brain") + const pathname = usePathname() + // Reachable via ?view=configure too, where the path carries no section. + const activeSection = + pathToConfigureSection(pathname) ?? DEFAULT_CONFIGURE_SECTION const active = SECTIONS.find((section) => section.id === activeSection) if (!active) return null @@ -90,11 +92,10 @@ export function ConfigureView() { const isActive = section.id === activeSection const Icon = section.icon return ( - + ) })} @@ -135,7 +136,7 @@ export function ConfigureView() {

} > - {activeSection === "company-brain" ? ( + {activeSection === "tools" ? ( ) : activeSection === "models" ? ( diff --git a/apps/web/lib/configure-routes.ts b/apps/web/lib/configure-routes.ts new file mode 100644 index 00000000..cdef17dd --- /dev/null +++ b/apps/web/lib/configure-routes.ts @@ -0,0 +1,37 @@ +// Sections under the /configure route. +// "tools" is the "Integrations" section, slugged to avoid clashing with /integrations. +export const CONFIGURE_SECTIONS = [ + "tools", + "models", + "workspace-prompt", + "proactivity", + "automations", +] as const + +export type ConfigureSection = (typeof CONFIGURE_SECTIONS)[number] + +export const DEFAULT_CONFIGURE_SECTION: ConfigureSection = "tools" + +export function isConfigureSection(slug: string): slug is ConfigureSection { + return (CONFIGURE_SECTIONS as readonly string[]).includes(slug) +} + +export function configureSectionToPath(section: ConfigureSection): string { + return section === DEFAULT_CONFIGURE_SECTION + ? "/configure" + : `/configure/${section}` +} + +export function pathToConfigureSection( + pathname: string, +): ConfigureSection | null { + const trimmed = pathname.replace(/\/$/, "") + if (trimmed === "/configure") return DEFAULT_CONFIGURE_SECTION + const slug = trimmed.match(/^\/configure\/([^/]+)$/)?.[1] + if (slug && isConfigureSection(slug)) return slug + return null +} + +export function isConfigurePath(pathname: string): boolean { + return pathToConfigureSection(pathname) !== null +} diff --git a/apps/web/lib/view-mode-context.tsx b/apps/web/lib/view-mode-context.tsx index d26fc731..0bc5f218 100644 --- a/apps/web/lib/view-mode-context.tsx +++ b/apps/web/lib/view-mode-context.tsx @@ -8,6 +8,7 @@ import { isIntegrationView, pathToIntegrationView, } from "@/lib/integration-routes" +import { isConfigurePath } from "@/lib/configure-routes" import { analytics } from "@/lib/analytics" import { useCallback, useEffect } from "react" @@ -33,8 +34,11 @@ export function useViewMode() { const router = useRouter() const [paramView, setParamView] = useQueryState("view", viewParam) - // On /integrations[/card] the path is the source of truth; elsewhere the ?view param is. - const pathView = pathToIntegrationView(pathname) + // On /integrations[/card] and /configure[/section] the path is the source of truth; + // elsewhere the ?view param is. + const pathView: ViewMode | null = + pathToIntegrationView(pathname) ?? + (isConfigurePath(pathname) ? "configure" : null) const viewMode: ViewMode = pathView ?? paramView const setViewMode = useCallback( @@ -44,8 +48,12 @@ export function useViewMode() { router.push(integrationViewToPath(mode)) return } - // Leaving (or already off) the integrations route for a non-integration view. - if (pathToIntegrationView(pathname)) { + if (mode === "configure") { + router.push("/configure") + return + } + // Leaving (or already off) a path-owned route for a param-owned view. + if (pathToIntegrationView(pathname) || isConfigurePath(pathname)) { router.push(mode === "dashboard" ? "/" : `/?view=${mode}`) return } @@ -57,8 +65,8 @@ export function useViewMode() { return { viewMode, setViewMode, isInitialized: true } } -// Forwards legacy /?view=integrations (and sub-views) to the canonical /integrations route, -// preserving any other query params. Call once near the app root. +// Forwards legacy /?view=integrations (and sub-views) and /?view=configure to their +// canonical routes, preserving any other query params. Call once near the app root. export function useLegacyViewRedirect() { const pathname = usePathname() const router = useRouter() @@ -67,10 +75,16 @@ export function useLegacyViewRedirect() { useEffect(() => { if (pathname !== "/") return const view = searchParams.get("view") - if (!view || !isIntegrationView(view)) return + if (!view) return + const target = isIntegrationView(view) + ? integrationViewToPath(view) + : view === "configure" + ? "/configure" + : null + if (!target) return const params = new URLSearchParams(searchParams.toString()) params.delete("view") const qs = params.toString() - router.replace(integrationViewToPath(view) + (qs ? `?${qs}` : "")) + router.replace(target + (qs ? `?${qs}` : "")) }, [pathname, searchParams, router]) }