mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
feat(web): create org from settings launches onboarding (#1136)
Settings 'create organization' now routes to /onboarding?new=1&name=... (team/personal, invites) instead of a bare authClient.create. Adds the forceCreate path with name prefill, clears new=1 after a successful create to prevent duplicate orgs, and fixes the Radix popover-to-dialog pointer-events lock.
This commit is contained in:
parent
504940414c
commit
ab3dcd7a73
2 changed files with 38 additions and 36 deletions
|
|
@ -44,6 +44,10 @@ export default function BrainOnboardingPage() {
|
|||
const { user, org, organizations, setActiveOrg, refetchOrganizations } =
|
||||
useAuth()
|
||||
|
||||
// `?new=1` forces creating an additional org even when the user already has one.
|
||||
const forceCreate = params?.get("new") === "1"
|
||||
const nameParam = params?.get("name")?.trim() || ""
|
||||
|
||||
const stepFromUrl = (params?.get("step") as BrainStep | null) ?? "about"
|
||||
const initialStep: BrainStep = BRAIN_STEPS.includes(stepFromUrl)
|
||||
? stepFromUrl
|
||||
|
|
@ -68,7 +72,7 @@ export default function BrainOnboardingPage() {
|
|||
const [about, setAbout] = useState<AboutValues>({
|
||||
name: user?.name ?? "",
|
||||
about: "",
|
||||
workspaceName: suggestedWorkspaceName,
|
||||
workspaceName: nameParam || suggestedWorkspaceName,
|
||||
workspaceDomain: domain ?? "",
|
||||
})
|
||||
const [sources, setSources] = useState<SourcesValues>({
|
||||
|
|
@ -82,6 +86,7 @@ export default function BrainOnboardingPage() {
|
|||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (forceCreate) return
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return
|
||||
|
|
@ -96,7 +101,7 @@ export default function BrainOnboardingPage() {
|
|||
if (cached.sources) setSources((s) => ({ ...s, ...cached.sources }))
|
||||
if (cached.team) setTeam((t) => ({ ...t, ...cached.team }))
|
||||
} catch {}
|
||||
}, [])
|
||||
}, [forceCreate])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
|
|
@ -175,8 +180,13 @@ export default function BrainOnboardingPage() {
|
|||
try {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
} catch {}
|
||||
// Extra org from settings: hard-reload so org-scoped caches don't show the previous org's data.
|
||||
if (forceCreate) {
|
||||
window.location.href = "/?onboarded=1"
|
||||
return
|
||||
}
|
||||
router.push("/?onboarded=1")
|
||||
}, [router, mode, sources, team])
|
||||
}, [router, mode, sources, team, forceCreate])
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
const idx = BRAIN_STEPS.indexOf(step)
|
||||
|
|
@ -193,7 +203,7 @@ export default function BrainOnboardingPage() {
|
|||
const creatingOrgRef = useRef(false)
|
||||
|
||||
const ensureOrg = useCallback(async () => {
|
||||
if (organizations && organizations.length > 0) return
|
||||
if (!forceCreate && organizations && organizations.length > 0) return
|
||||
const name = (about.workspaceName || suggestedWorkspaceName).trim()
|
||||
const slug = generateOrgSlug(name)
|
||||
const metadata: BrainMetadata & { signupSource: string } = {
|
||||
|
|
@ -225,6 +235,13 @@ export default function BrainOnboardingPage() {
|
|||
has_about: Boolean(about.about.trim()),
|
||||
has_domain: Boolean(mode === "team" && (about.workspaceDomain || domain)),
|
||||
})
|
||||
// Drop new=1 so a reload or back+Continue reuses this org instead of creating a duplicate.
|
||||
if (forceCreate) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete("new")
|
||||
url.searchParams.delete("name")
|
||||
router.replace(url.pathname + url.search, { scroll: false })
|
||||
}
|
||||
}, [
|
||||
organizations,
|
||||
about,
|
||||
|
|
@ -234,6 +251,8 @@ export default function BrainOnboardingPage() {
|
|||
containerTag,
|
||||
setActiveOrg,
|
||||
refetchOrganizations,
|
||||
forceCreate,
|
||||
router,
|
||||
])
|
||||
|
||||
const handleAboutContinue = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { authClient } from "@lib/auth"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog"
|
||||
import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge"
|
||||
|
|
@ -23,17 +23,9 @@ import { useTokenUsage, type PlanType } from "@/hooks/use-token-usage"
|
|||
const SURFACE_SHADOW =
|
||||
"0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset"
|
||||
|
||||
function generateOrgSlug(name: string): string {
|
||||
const base =
|
||||
name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "") || "org"
|
||||
return `${base}-${Math.floor(100000 + Math.random() * 900000)}`
|
||||
}
|
||||
|
||||
export function SettingsOrgSwitcher() {
|
||||
const { org, organizations, setActiveOrg } = useAuth()
|
||||
const router = useRouter()
|
||||
const autumn = useCustomer()
|
||||
const { currentPlan } = useTokenUsage(autumn)
|
||||
const { data: orgSummaries } = useOrgSummaries()
|
||||
|
|
@ -44,6 +36,11 @@ export function SettingsOrgSwitcher() {
|
|||
const [createName, setCreateName] = useState("")
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
// Clear a stale Radix `pointer-events: none` left on <body> so the dialog accepts clicks.
|
||||
useEffect(() => {
|
||||
if (createOpen) document.body.style.pointerEvents = ""
|
||||
}, [createOpen])
|
||||
|
||||
const planByOrgId = useMemo(() => {
|
||||
const map = new Map<string, PlanType>()
|
||||
for (const summary of orgSummaries ?? []) {
|
||||
|
|
@ -78,29 +75,14 @@ export function SettingsOrgSwitcher() {
|
|||
}
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
const handleCreate = () => {
|
||||
const name = createName.trim()
|
||||
if (!name || creating) return
|
||||
setCreating(true)
|
||||
try {
|
||||
const result = await authClient.organization.create({
|
||||
name,
|
||||
slug: generateOrgSlug(name),
|
||||
metadata: { signupSource: "consumer" },
|
||||
})
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message ?? "Failed to create organization")
|
||||
}
|
||||
await setActiveOrg(result.data?.slug ?? "")
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
setCreating(false)
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to create organization",
|
||||
)
|
||||
}
|
||||
// Org creation now happens through the onboarding flow (team/personal, name, invites).
|
||||
setCreateOpen(false)
|
||||
setOpen(false)
|
||||
router.push(`/onboarding?new=1&name=${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -179,8 +161,9 @@ export function SettingsOrgSwitcher() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Defer dialog open: same-tick handoff leaves Radix's pointer-events stuck on <body>.
|
||||
setOpen(false)
|
||||
setCreateOpen(true)
|
||||
setTimeout(() => setCreateOpen(true), 120)
|
||||
}}
|
||||
className="w-full flex items-center gap-2.5 rounded-[10px] px-3 py-2 text-left text-[#A3A3A3] transition-colors hover:bg-white/5 hover:text-white cursor-pointer"
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue