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:
Waleed Latif
2025-07-16 17:39:39 -07:00
committed by GitHub
parent e142753d64
commit 60e905c520
2 changed files with 154 additions and 34 deletions

View File

@@ -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>

View File

@@ -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>