mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
feat(account): added account to settings
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { ChevronDown, LogOut, Plus, User, UserPlus } from 'lucide-react'
|
||||
import { AgentIcon } from '@/components/icons'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { signOut, useSession } from '@/lib/auth-client'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { clearUserData } from '@/stores'
|
||||
|
||||
interface AccountProps {
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
// Mock user data - in a real app, this would come from an auth provider
|
||||
interface UserData {
|
||||
isLoggedIn: boolean
|
||||
name?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
interface AccountData {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export function Account({ onOpenChange }: AccountProps) {
|
||||
const router = useRouter()
|
||||
|
||||
// In a real app, this would be fetched from an auth provider
|
||||
const [userData, setUserData] = useState<UserData>({
|
||||
isLoggedIn: false,
|
||||
name: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
// Get session data using the client hook
|
||||
const { data: session, isPending, error } = useSession()
|
||||
const [isLoadingUserData, setIsLoadingUserData] = useState(false)
|
||||
|
||||
// Mock accounts for the multi-account UI
|
||||
const [accounts, setAccounts] = useState<AccountData[]>([])
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// Update user data when session changes
|
||||
useEffect(() => {
|
||||
const updateUserData = async () => {
|
||||
if (!isPending && session?.user) {
|
||||
// User is logged in
|
||||
setUserData({
|
||||
isLoggedIn: true,
|
||||
name: session.user.name || 'User',
|
||||
email: session.user.email,
|
||||
})
|
||||
|
||||
setAccounts([
|
||||
{
|
||||
id: '1',
|
||||
name: session.user.name || 'User',
|
||||
email: session.user.email,
|
||||
isActive: true,
|
||||
},
|
||||
])
|
||||
} else if (!isPending) {
|
||||
// User is not logged in
|
||||
setUserData({
|
||||
isLoggedIn: false,
|
||||
name: '',
|
||||
email: '',
|
||||
})
|
||||
setAccounts([])
|
||||
}
|
||||
}
|
||||
|
||||
updateUserData()
|
||||
}, [session, isPending])
|
||||
|
||||
const handleSignIn = () => {
|
||||
// Use Next.js router to navigate to login page
|
||||
router.push('/login')
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
// Start the sign-out process
|
||||
const signOutPromise = signOut()
|
||||
|
||||
// Clear all user data to prevent persistence between accounts
|
||||
await clearUserData()
|
||||
|
||||
// Set a short timeout to improve perceived performance
|
||||
// while still ensuring auth state starts to clear
|
||||
setTimeout(() => {
|
||||
router.push('/login?fromLogout=true')
|
||||
}, 100)
|
||||
|
||||
// Still wait for the promise to resolve/reject to catch errors
|
||||
await signOutPromise
|
||||
} catch (error) {
|
||||
console.error('Error signing out:', error)
|
||||
// Still navigate even if there's an error
|
||||
router.push('/login?fromLogout=true')
|
||||
} finally {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const activeAccount = accounts.find((acc) => acc.isActive) || accounts[0]
|
||||
|
||||
// Loading animation component
|
||||
const LoadingAccountBlock = () => (
|
||||
<div className="group flex items-center justify-between gap-3 rounded-lg border bg-card p-4 shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted animate-pulse">
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-[shimmer_1.5s_infinite]"
|
||||
style={{ transform: 'translateX(-100%)', animation: 'shimmer 1.5s infinite' }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="h-4 w-24 bg-muted rounded animate-pulse"></div>
|
||||
<div className="h-3 w-32 bg-muted rounded animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-4 w-4 bg-muted rounded"></div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Account</h3>
|
||||
</div>
|
||||
|
||||
{/* Account Dropdown Component */}
|
||||
<div className="max-w-xs">
|
||||
<div className="relative">
|
||||
{isPending || isLoadingUserData ? (
|
||||
<LoadingAccountBlock />
|
||||
) : (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'group flex items-center justify-between gap-3 rounded-lg border bg-card p-4 shadow-sm transition-all cursor-pointer',
|
||||
'hover:bg-accent/50 hover:shadow-md',
|
||||
open && 'bg-accent/50 shadow-md'
|
||||
)}
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-blue-500">
|
||||
{userData.isLoggedIn ? (
|
||||
<div className="h-full w-full flex items-center justify-center bg-[#7F2FFF]">
|
||||
<AgentIcon className="text-white transition-transform duration-200 group-hover:scale-110 -translate-y-[0.5px]" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-500 h-full w-full flex items-center justify-center">
|
||||
<AgentIcon className="text-white transition-transform duration-200 group-hover:scale-110" />
|
||||
</div>
|
||||
)}
|
||||
{userData.isLoggedIn && accounts.length > 1 && (
|
||||
<div className="absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
|
||||
{accounts.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 mb-[-2px]">
|
||||
<h3 className="font-medium leading-none truncate max-w-[160px]">
|
||||
{userData.isLoggedIn ? activeAccount?.name : 'Sign in'}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground truncate max-w-[160px]">
|
||||
{userData.isLoggedIn ? activeAccount?.email : 'Click to sign in'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 text-muted-foreground transition-transform',
|
||||
open && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-[300px] max-h-[350px] overflow-y-auto"
|
||||
sideOffset={8}
|
||||
>
|
||||
{userData.isLoggedIn ? (
|
||||
<>
|
||||
{accounts.length > 1 && (
|
||||
<>
|
||||
<div className="mb-2 px-2 py-1.5 text-sm font-medium text-muted-foreground">
|
||||
Switch Account
|
||||
</div>
|
||||
{accounts.map((account) => (
|
||||
<DropdownMenuItem
|
||||
key={account.id}
|
||||
className={cn(
|
||||
'flex items-center gap-2 p-3 cursor-pointer',
|
||||
account.isActive && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<div className="relative flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#7F2FFF]">
|
||||
<User className="text-white w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium leading-none">{account.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{account.email}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 p-3 cursor-pointer text-destructive focus:text-destructive"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span>Sign Out</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 p-3 cursor-pointer"
|
||||
onClick={handleSignIn}
|
||||
>
|
||||
<UserPlus className="h-4 w-4" />
|
||||
<span>Sign in</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export function EnvironmentVariables({ onOpenChange }: EnvironmentVariablesProps
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Fixed Header */}
|
||||
<div className="px-6 pt-6">
|
||||
<h2 className="text-lg font-medium mb-4">Environment Variables</h2>
|
||||
<h2 className="text-lg font-medium mb-6">Environment Variables</h2>
|
||||
<div className={`${GRID_COLS} px-0.5 mb-2`}>
|
||||
<Label>Key</Label>
|
||||
<Label>Value</Label>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function General() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium mb-4">General Settings</h2>
|
||||
<h2 className="text-lg font-medium mb-[22px]">General Settings</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<Label htmlFor="theme-select" className="font-medium">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { KeyRound, Settings } from 'lucide-react'
|
||||
import { KeyRound, Settings, UserCircle } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SettingsNavigationProps {
|
||||
activeSection: string
|
||||
onSectionChange: (section: 'general' | 'environment') => void
|
||||
onSectionChange: (section: 'general' | 'environment' | 'account') => void
|
||||
}
|
||||
|
||||
const navigationItems = [
|
||||
@@ -17,6 +17,11 @@ const navigationItems = [
|
||||
label: 'Environment',
|
||||
icon: KeyRound,
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
label: 'Account',
|
||||
icon: UserCircle,
|
||||
},
|
||||
] as const
|
||||
|
||||
export function SettingsNavigation({ activeSection, onSectionChange }: SettingsNavigationProps) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Account } from './components/account/account'
|
||||
import { EnvironmentVariables } from './components/environment/environment'
|
||||
import { General } from './components/general/general'
|
||||
import { SettingsNavigation } from './components/settings-navigation/settings-navigation'
|
||||
@@ -14,7 +15,7 @@ interface SettingsModalProps {
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
type SettingsSection = 'general' | 'environment'
|
||||
type SettingsSection = 'general' | 'environment' | 'account'
|
||||
|
||||
export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('general')
|
||||
@@ -51,6 +52,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
<div className={cn('h-full', activeSection === 'environment' ? 'block' : 'hidden')}>
|
||||
<EnvironmentVariables onOpenChange={onOpenChange} />
|
||||
</div>
|
||||
<div className={cn('h-full', activeSection === 'account' ? 'block' : 'hidden')}>
|
||||
<Account onOpenChange={onOpenChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -26,9 +26,9 @@ export function ToolbarBlock({ config }: ToolbarBlockProps) {
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col gap-1 mb-[-2px]">
|
||||
<h3 className="font-medium leading-none">{config.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{config.description}</p>
|
||||
<p className="text-sm text-muted-foreground leading-snug">{config.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
25
components/ui/separator.tsx
Normal file
25
components/ui/separator.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
47
package-lock.json
generated
47
package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
@@ -2554,6 +2555,52 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
|
||||
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCustomToolsStore } from './custom-tools/store'
|
||||
import { useExecutionStore } from './execution/store'
|
||||
import { useNotificationStore } from './notifications/store'
|
||||
import { useEnvironmentStore } from './settings/environment/store'
|
||||
import { getSyncManagers, initializeSyncManagers } from './sync-registry'
|
||||
import { getSyncManagers, initializeSyncManagers, resetSyncManagers } from './sync-registry'
|
||||
import {
|
||||
loadRegistry,
|
||||
loadSubblockValues,
|
||||
@@ -145,6 +145,31 @@ function cleanupApplication(): void {
|
||||
getSyncManagers().forEach((manager) => manager.dispose())
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all user data when signing out
|
||||
* This ensures data from one account doesn't persist to another
|
||||
*/
|
||||
export async function clearUserData(): Promise<void> {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
// 1. Reset all sync managers to prevent any pending syncs
|
||||
resetSyncManagers()
|
||||
|
||||
// 2. Reset all stores to their initial state
|
||||
resetAllStores()
|
||||
|
||||
// 3. Clear localStorage except for essential app settings
|
||||
const keysToKeep = ['next-favicon', 'theme']
|
||||
const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key))
|
||||
|
||||
console.log('User data cleared successfully')
|
||||
} catch (error) {
|
||||
console.error('Error clearing user data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage application lifecycle
|
||||
*/
|
||||
@@ -159,6 +184,16 @@ export function useAppInitialization() {
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to reinitialize the application after successful login
|
||||
* Use this in the login success handler or post-login page
|
||||
*/
|
||||
export function useLoginInitialization() {
|
||||
useEffect(() => {
|
||||
reinitializeAfterLogin()
|
||||
}, [])
|
||||
}
|
||||
|
||||
// Initialize immediately when imported on client
|
||||
if (typeof window !== 'undefined') {
|
||||
initializeApplication()
|
||||
@@ -178,13 +213,6 @@ export {
|
||||
|
||||
// Helper function to reset all stores
|
||||
export const resetAllStores = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Selectively clear localStorage items
|
||||
const keysToKeep = ['next-favicon']
|
||||
const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key))
|
||||
}
|
||||
|
||||
// Reset all stores to initial state
|
||||
useWorkflowRegistry.setState({
|
||||
workflows: {},
|
||||
@@ -229,3 +257,23 @@ export const logAllStores = () => {
|
||||
|
||||
// Re-export sync managers
|
||||
export { workflowSync, environmentSync } from './sync-registry'
|
||||
|
||||
/**
|
||||
* Reinitialize the application after login
|
||||
* This ensures we load fresh data from the database for the new user
|
||||
*/
|
||||
export async function reinitializeAfterLogin(): Promise<void> {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
// Reset initialization flags to force a fresh load
|
||||
isInitializing = false
|
||||
|
||||
// Reinitialize the application
|
||||
await initializeApplication()
|
||||
|
||||
console.log('Application reinitialized after login')
|
||||
} catch (error) {
|
||||
console.error('Error reinitializing application:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +69,21 @@ export function getSyncManagers(): SyncManager[] {
|
||||
return managers
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all sync managers
|
||||
* This is used during sign-out to ensure clean state for the next user
|
||||
*/
|
||||
export function resetSyncManagers(): void {
|
||||
// Dispose all existing managers
|
||||
managers.forEach((manager) => manager.dispose())
|
||||
|
||||
// Reset the managers array
|
||||
managers = []
|
||||
|
||||
// Reset initialization flags
|
||||
initialized = false
|
||||
initializing = false
|
||||
}
|
||||
|
||||
// Export individual sync managers for direct use
|
||||
export { workflowSync, environmentSync }
|
||||
|
||||
Reference in New Issue
Block a user