mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(workspace): add ability to leave joined workspaces (#713)
* feat(workspace): add ability to leave joined workspaces * renamed workspaces/members/[id] to workspaces/members/[userId] * revert name change for route
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Plus, Send, Trash2 } from 'lucide-react'
|
||||
import { LogOut, Plus, Send, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -43,7 +43,9 @@ interface WorkspaceSelectorProps {
|
||||
onSwitchWorkspace: (workspace: Workspace) => Promise<void>
|
||||
onCreateWorkspace: () => Promise<void>
|
||||
onDeleteWorkspace: (workspace: Workspace) => Promise<void>
|
||||
onLeaveWorkspace: (workspace: Workspace) => Promise<void>
|
||||
isDeleting: boolean
|
||||
isLeaving: boolean
|
||||
}
|
||||
|
||||
export function WorkspaceSelector({
|
||||
@@ -54,7 +56,9 @@ export function WorkspaceSelector({
|
||||
onSwitchWorkspace,
|
||||
onCreateWorkspace,
|
||||
onDeleteWorkspace,
|
||||
onLeaveWorkspace,
|
||||
isDeleting,
|
||||
isLeaving,
|
||||
}: WorkspaceSelectorProps) {
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
@@ -94,6 +98,16 @@ export function WorkspaceSelector({
|
||||
[onDeleteWorkspace]
|
||||
)
|
||||
|
||||
/**
|
||||
* Confirm leave workspace
|
||||
*/
|
||||
const confirmLeaveWorkspace = useCallback(
|
||||
async (workspaceToLeave: Workspace) => {
|
||||
await onLeaveWorkspace(workspaceToLeave)
|
||||
},
|
||||
[onLeaveWorkspace]
|
||||
)
|
||||
|
||||
// Render workspace list
|
||||
const renderWorkspaceList = () => {
|
||||
if (isWorkspacesLoading) {
|
||||
@@ -133,40 +147,83 @@ export function WorkspaceSelector({
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex h-full w-6 flex-shrink-0 items-center justify-center'>
|
||||
{hoveredWorkspaceId === workspace.id && workspace.permissions === 'admin' && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className='h-4 w-4 p-0 text-muted-foreground transition-colors hover:text-muted-foreground'
|
||||
>
|
||||
<Trash2 className='h-2 w-2' />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
{hoveredWorkspaceId === workspace.id && (
|
||||
<>
|
||||
{/* Leave Workspace - for non-admin users */}
|
||||
{workspace.permissions !== 'admin' && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className='h-4 w-4 p-0 text-muted-foreground transition-colors hover:text-muted-foreground'
|
||||
>
|
||||
<LogOut className='h-2 w-2' />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Workspace</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{workspace.name}"? This action cannot be
|
||||
undone and will permanently delete all workflows and data in this workspace.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => confirmDeleteWorkspace(workspace)}
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Leave Workspace</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to leave "{workspace.name}"? You will lose access
|
||||
to all workflows and data in this workspace.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => confirmLeaveWorkspace(workspace)}
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
disabled={isLeaving}
|
||||
>
|
||||
{isLeaving ? 'Leaving...' : 'Leave'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
|
||||
{/* Delete Workspace - for admin users */}
|
||||
{workspace.permissions === 'admin' && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className='h-4 w-4 p-0 text-muted-foreground transition-colors hover:text-muted-foreground'
|
||||
>
|
||||
<Trash2 className='h-2 w-2' />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Workspace</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{workspace.name}"? This action cannot
|
||||
be undone and will permanently delete all workflows and data in this
|
||||
workspace.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => confirmDeleteWorkspace(workspace)}
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,6 +117,7 @@ export function Sidebar() {
|
||||
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null)
|
||||
const [isWorkspacesLoading, setIsWorkspacesLoading] = useState(true)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [isLeaving, setIsLeaving] = useState(false)
|
||||
|
||||
// Update activeWorkspace ref when state changes
|
||||
activeWorkspaceRef.current = activeWorkspace
|
||||
@@ -361,6 +362,66 @@ export function Sidebar() {
|
||||
[fetchWorkspaces, refreshWorkspaceList, workspaces, switchWorkspace]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle leave workspace
|
||||
*/
|
||||
const handleLeaveWorkspace = useCallback(
|
||||
async (workspaceToLeave: Workspace) => {
|
||||
setIsLeaving(true)
|
||||
try {
|
||||
logger.info('Leaving workspace:', workspaceToLeave.id)
|
||||
|
||||
// Use the existing member removal API with current user's ID
|
||||
const response = await fetch(`/api/workspaces/members/${sessionData?.user?.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId: workspaceToLeave.id,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to leave workspace')
|
||||
}
|
||||
|
||||
logger.info('Left workspace successfully:', workspaceToLeave.id)
|
||||
|
||||
// Check if we're leaving the current workspace (either active or in URL)
|
||||
const isLeavingCurrentWorkspace =
|
||||
workspaceIdRef.current === workspaceToLeave.id ||
|
||||
activeWorkspaceRef.current?.id === workspaceToLeave.id
|
||||
|
||||
if (isLeavingCurrentWorkspace) {
|
||||
// For current workspace leaving, use full fetchWorkspaces with URL validation
|
||||
logger.info(
|
||||
'Leaving current workspace - using full workspace refresh with URL validation'
|
||||
)
|
||||
await fetchWorkspaces()
|
||||
|
||||
// If we left the active workspace, switch to the first available workspace
|
||||
if (activeWorkspaceRef.current?.id === workspaceToLeave.id) {
|
||||
const remainingWorkspaces = workspaces.filter((w) => w.id !== workspaceToLeave.id)
|
||||
if (remainingWorkspaces.length > 0) {
|
||||
await switchWorkspace(remainingWorkspaces[0])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For non-current workspace leaving, just refresh the list without URL validation
|
||||
logger.info('Leaving non-current workspace - using simple list refresh')
|
||||
await refreshWorkspaceList()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error leaving workspace:', error)
|
||||
} finally {
|
||||
setIsLeaving(false)
|
||||
}
|
||||
},
|
||||
[fetchWorkspaces, refreshWorkspaceList, workspaces, switchWorkspace, sessionData?.user?.id]
|
||||
)
|
||||
|
||||
/**
|
||||
* Validate workspace exists before making API calls
|
||||
*/
|
||||
@@ -688,7 +749,9 @@ export function Sidebar() {
|
||||
onSwitchWorkspace={switchWorkspace}
|
||||
onCreateWorkspace={handleCreateWorkspace}
|
||||
onDeleteWorkspace={confirmDeleteWorkspace}
|
||||
onLeaveWorkspace={handleLeaveWorkspace}
|
||||
isDeleting={isDeleting}
|
||||
isLeaving={isLeaving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user