Skip to content

Commit 60e905c

Browse files
authored
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
1 parent e142753 commit 60e905c

File tree

2 files changed

+155
-35
lines changed

2 files changed

+155
-35
lines changed

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-selector/workspace-selector.tsx

Lines changed: 92 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useCallback, useEffect, useRef, useState } from 'react'
4-
import { Plus, Send, Trash2 } from 'lucide-react'
4+
import { LogOut, Plus, Send, Trash2 } from 'lucide-react'
55
import {
66
AlertDialog,
77
AlertDialogAction,
@@ -43,7 +43,9 @@ interface WorkspaceSelectorProps {
4343
onSwitchWorkspace: (workspace: Workspace) => Promise<void>
4444
onCreateWorkspace: () => Promise<void>
4545
onDeleteWorkspace: (workspace: Workspace) => Promise<void>
46+
onLeaveWorkspace: (workspace: Workspace) => Promise<void>
4647
isDeleting: boolean
48+
isLeaving: boolean
4749
}
4850

4951
export function WorkspaceSelector({
@@ -54,7 +56,9 @@ export function WorkspaceSelector({
5456
onSwitchWorkspace,
5557
onCreateWorkspace,
5658
onDeleteWorkspace,
59+
onLeaveWorkspace,
5760
isDeleting,
61+
isLeaving,
5862
}: WorkspaceSelectorProps) {
5963
const userPermissions = useUserPermissionsContext()
6064

@@ -94,6 +98,16 @@ export function WorkspaceSelector({
9498
[onDeleteWorkspace]
9599
)
96100

101+
/**
102+
* Confirm leave workspace
103+
*/
104+
const confirmLeaveWorkspace = useCallback(
105+
async (workspaceToLeave: Workspace) => {
106+
await onLeaveWorkspace(workspaceToLeave)
107+
},
108+
[onLeaveWorkspace]
109+
)
110+
97111
// Render workspace list
98112
const renderWorkspaceList = () => {
99113
if (isWorkspacesLoading) {
@@ -133,40 +147,83 @@ export function WorkspaceSelector({
133147
</span>
134148
</div>
135149
<div className='flex h-full w-6 flex-shrink-0 items-center justify-center'>
136-
{hoveredWorkspaceId === workspace.id && workspace.permissions === 'admin' && (
137-
<AlertDialog>
138-
<AlertDialogTrigger asChild>
139-
<Button
140-
variant='ghost'
141-
onClick={(e) => {
142-
e.stopPropagation()
143-
}}
144-
className='h-4 w-4 p-0 text-muted-foreground transition-colors hover:text-muted-foreground'
145-
>
146-
<Trash2 className='h-2 w-2' />
147-
</Button>
148-
</AlertDialogTrigger>
149-
150-
<AlertDialogContent>
151-
<AlertDialogHeader>
152-
<AlertDialogTitle>Delete Workspace</AlertDialogTitle>
153-
<AlertDialogDescription>
154-
Are you sure you want to delete "{workspace.name}"? This action cannot be
155-
undone and will permanently delete all workflows and data in this workspace.
156-
</AlertDialogDescription>
157-
</AlertDialogHeader>
158-
<AlertDialogFooter>
159-
<AlertDialogCancel>Cancel</AlertDialogCancel>
160-
<AlertDialogAction
161-
onClick={() => confirmDeleteWorkspace(workspace)}
162-
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
163-
disabled={isDeleting}
164-
>
165-
{isDeleting ? 'Deleting...' : 'Delete'}
166-
</AlertDialogAction>
167-
</AlertDialogFooter>
168-
</AlertDialogContent>
169-
</AlertDialog>
150+
{hoveredWorkspaceId === workspace.id && (
151+
<>
152+
{/* Leave Workspace - for non-admin users */}
153+
{workspace.permissions !== 'admin' && (
154+
<AlertDialog>
155+
<AlertDialogTrigger asChild>
156+
<Button
157+
variant='ghost'
158+
onClick={(e) => {
159+
e.stopPropagation()
160+
}}
161+
className='h-4 w-4 p-0 text-muted-foreground transition-colors hover:text-muted-foreground'
162+
>
163+
<LogOut className='h-2 w-2' />
164+
</Button>
165+
</AlertDialogTrigger>
166+
167+
<AlertDialogContent>
168+
<AlertDialogHeader>
169+
<AlertDialogTitle>Leave Workspace</AlertDialogTitle>
170+
<AlertDialogDescription>
171+
Are you sure you want to leave "{workspace.name}"? You will lose access
172+
to all workflows and data in this workspace.
173+
</AlertDialogDescription>
174+
</AlertDialogHeader>
175+
<AlertDialogFooter>
176+
<AlertDialogCancel>Cancel</AlertDialogCancel>
177+
<AlertDialogAction
178+
onClick={() => confirmLeaveWorkspace(workspace)}
179+
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
180+
disabled={isLeaving}
181+
>
182+
{isLeaving ? 'Leaving...' : 'Leave'}
183+
</AlertDialogAction>
184+
</AlertDialogFooter>
185+
</AlertDialogContent>
186+
</AlertDialog>
187+
)}
188+
189+
{/* Delete Workspace - for admin users */}
190+
{workspace.permissions === 'admin' && (
191+
<AlertDialog>
192+
<AlertDialogTrigger asChild>
193+
<Button
194+
variant='ghost'
195+
onClick={(e) => {
196+
e.stopPropagation()
197+
}}
198+
className='h-4 w-4 p-0 text-muted-foreground transition-colors hover:text-muted-foreground'
199+
>
200+
<Trash2 className='h-2 w-2' />
201+
</Button>
202+
</AlertDialogTrigger>
203+
204+
<AlertDialogContent>
205+
<AlertDialogHeader>
206+
<AlertDialogTitle>Delete Workspace</AlertDialogTitle>
207+
<AlertDialogDescription>
208+
Are you sure you want to delete "{workspace.name}"? This action cannot
209+
be undone and will permanently delete all workflows and data in this
210+
workspace.
211+
</AlertDialogDescription>
212+
</AlertDialogHeader>
213+
<AlertDialogFooter>
214+
<AlertDialogCancel>Cancel</AlertDialogCancel>
215+
<AlertDialogAction
216+
onClick={() => confirmDeleteWorkspace(workspace)}
217+
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
218+
disabled={isDeleting}
219+
>
220+
{isDeleting ? 'Deleting...' : 'Delete'}
221+
</AlertDialogAction>
222+
</AlertDialogFooter>
223+
</AlertDialogContent>
224+
</AlertDialog>
225+
)}
226+
</>
170227
)}
171228
</div>
172229
</div>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export function Sidebar() {
117117
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null)
118118
const [isWorkspacesLoading, setIsWorkspacesLoading] = useState(true)
119119
const [isDeleting, setIsDeleting] = useState(false)
120+
const [isLeaving, setIsLeaving] = useState(false)
120121

121122
// Update activeWorkspace ref when state changes
122123
activeWorkspaceRef.current = activeWorkspace
@@ -361,6 +362,66 @@ export function Sidebar() {
361362
[fetchWorkspaces, refreshWorkspaceList, workspaces, switchWorkspace]
362363
)
363364

365+
/**
366+
* Handle leave workspace
367+
*/
368+
const handleLeaveWorkspace = useCallback(
369+
async (workspaceToLeave: Workspace) => {
370+
setIsLeaving(true)
371+
try {
372+
logger.info('Leaving workspace:', workspaceToLeave.id)
373+
374+
// Use the existing member removal API with current user's ID
375+
const response = await fetch(`/api/workspaces/members/${sessionData?.user?.id}`, {
376+
method: 'DELETE',
377+
headers: {
378+
'Content-Type': 'application/json',
379+
},
380+
body: JSON.stringify({
381+
workspaceId: workspaceToLeave.id,
382+
}),
383+
})
384+
385+
if (!response.ok) {
386+
const errorData = await response.json()
387+
throw new Error(errorData.error || 'Failed to leave workspace')
388+
}
389+
390+
logger.info('Left workspace successfully:', workspaceToLeave.id)
391+
392+
// Check if we're leaving the current workspace (either active or in URL)
393+
const isLeavingCurrentWorkspace =
394+
workspaceIdRef.current === workspaceToLeave.id ||
395+
activeWorkspaceRef.current?.id === workspaceToLeave.id
396+
397+
if (isLeavingCurrentWorkspace) {
398+
// For current workspace leaving, use full fetchWorkspaces with URL validation
399+
logger.info(
400+
'Leaving current workspace - using full workspace refresh with URL validation'
401+
)
402+
await fetchWorkspaces()
403+
404+
// If we left the active workspace, switch to the first available workspace
405+
if (activeWorkspaceRef.current?.id === workspaceToLeave.id) {
406+
const remainingWorkspaces = workspaces.filter((w) => w.id !== workspaceToLeave.id)
407+
if (remainingWorkspaces.length > 0) {
408+
await switchWorkspace(remainingWorkspaces[0])
409+
}
410+
}
411+
} else {
412+
// For non-current workspace leaving, just refresh the list without URL validation
413+
logger.info('Leaving non-current workspace - using simple list refresh')
414+
await refreshWorkspaceList()
415+
}
416+
} catch (error) {
417+
logger.error('Error leaving workspace:', error)
418+
} finally {
419+
setIsLeaving(false)
420+
}
421+
},
422+
[fetchWorkspaces, refreshWorkspaceList, workspaces, switchWorkspace, sessionData?.user?.id]
423+
)
424+
364425
/**
365426
* Validate workspace exists before making API calls
366427
*/
@@ -688,7 +749,9 @@ export function Sidebar() {
688749
onSwitchWorkspace={switchWorkspace}
689750
onCreateWorkspace={handleCreateWorkspace}
690751
onDeleteWorkspace={confirmDeleteWorkspace}
752+
onLeaveWorkspace={handleLeaveWorkspace}
691753
isDeleting={isDeleting}
754+
isLeaving={isLeaving}
692755
/>
693756
</div>
694757

0 commit comments

Comments
 (0)