Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/sim/app/api/users/me/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const SettingsSchema = z.object({
errorNotificationsEnabled: z.boolean().optional(),
snapToGridSize: z.number().min(0).max(50).optional(),
showActionBar: z.boolean().optional(),
lastActiveWorkspaceId: z.string().optional(),
})

const defaultSettings = {
Expand All @@ -41,6 +42,7 @@ const defaultSettings = {
errorNotificationsEnabled: true,
snapToGridSize: 0,
showActionBar: true,
lastActiveWorkspaceId: null,
}

export async function GET() {
Expand Down Expand Up @@ -76,6 +78,7 @@ export async function GET() {
errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true,
snapToGridSize: userSettings.snapToGridSize ?? 0,
showActionBar: userSettings.showActionBar ?? true,
lastActiveWorkspaceId: userSettings.lastActiveWorkspaceId ?? null,
},
},
{ status: 200 }
Expand Down
31 changes: 24 additions & 7 deletions apps/sim/app/api/workspaces/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,17 +267,34 @@ export async function DELETE(
}

try {
const [[workspaceRecord], totalWorkspaces] = await Promise.all([
db
.select({ name: workspace.name })
.from(workspace)
.where(and(eq(workspace.id, workspaceId), isNull(workspace.archivedAt)))
.limit(1),
db
.select({ id: permissions.entityId })
.from(permissions)
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
.where(
and(
eq(permissions.userId, session.user.id),
eq(permissions.entityType, 'workspace'),
isNull(workspace.archivedAt)
)
),
])

/** Counts all workspace memberships (any role), not just admin — prevents the user from reaching a zero-workspace state. */
if (totalWorkspaces.length <= 1) {
return NextResponse.json({ error: 'Cannot delete the only workspace' }, { status: 400 })
}

logger.info(
`Deleting workspace ${workspaceId} for user ${session.user.id}, deleteTemplates: ${deleteTemplates}`
)

// Fetch workspace name before deletion for audit logging
const [workspaceRecord] = await db
.select({ name: workspace.name })
.from(workspace)
.where(and(eq(workspace.id, workspaceId), isNull(workspace.archivedAt)))
.limit(1)

const workspaceWorkflows = await db
.select({ id: workflow.id })
.from(workflow)
Expand Down
68 changes: 41 additions & 27 deletions apps/sim/app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { db } from '@sim/db'
import { permissions, workflow, workspace } from '@sim/db/schema'
import { permissions, settings, workflow, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, desc, eq, isNull, sql } from 'drizzle-orm'
import { NextResponse } from 'next/server'
Expand Down Expand Up @@ -38,36 +38,47 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Invalid scope' }, { status: 400 })
}

const userWorkspaces = await db
.select({
workspace: workspace,
permissionType: permissions.permissionType,
})
.from(permissions)
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
.where(
scope === 'all'
? and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace'))
: scope === 'archived'
? and(
eq(permissions.userId, session.user.id),
eq(permissions.entityType, 'workspace'),
sql`${workspace.archivedAt} IS NOT NULL`
)
: and(
eq(permissions.userId, session.user.id),
eq(permissions.entityType, 'workspace'),
isNull(workspace.archivedAt)
)
)
.orderBy(desc(workspace.createdAt))
const settingsQuery = db
.select({ lastActiveWorkspaceId: settings.lastActiveWorkspaceId })
.from(settings)
.where(eq(settings.userId, session.user.id))
.limit(1)

const [userWorkspaces, userSettings] = await Promise.all([
db
.select({
workspace: workspace,
permissionType: permissions.permissionType,
})
.from(permissions)
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
.where(
scope === 'all'
? and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace'))
: scope === 'archived'
? and(
eq(permissions.userId, session.user.id),
eq(permissions.entityType, 'workspace'),
sql`${workspace.archivedAt} IS NOT NULL`
)
: and(
eq(permissions.userId, session.user.id),
eq(permissions.entityType, 'workspace'),
isNull(workspace.archivedAt)
)
)
.orderBy(desc(workspace.createdAt)),
settingsQuery,
])

const lastActiveWorkspaceId = userSettings[0]?.lastActiveWorkspaceId ?? null

if (scope === 'active' && userWorkspaces.length === 0) {
const defaultWorkspace = await createDefaultWorkspace(session.user.id, session.user.name)

await migrateExistingWorkflows(session.user.id, defaultWorkspace.id)

return NextResponse.json({ workspaces: [defaultWorkspace] })
return NextResponse.json({ workspaces: [defaultWorkspace], lastActiveWorkspaceId })
}

if (scope === 'active') {
Expand All @@ -77,12 +88,15 @@ export async function GET(request: Request) {
const workspacesWithPermissions = userWorkspaces.map(
({ workspace: workspaceDetails, permissionType }) => ({
...workspaceDetails,
role: permissionType === 'admin' ? 'owner' : 'member', // Map admin to owner for compatibility
role: permissionType === 'admin' ? 'owner' : 'member',
permissions: permissionType,
})
)

return NextResponse.json({ workspaces: workspacesWithPermissions })
return NextResponse.json({
workspaces: workspacesWithPermissions,
lastActiveWorkspaceId,
})
}

// POST /api/workspaces - Create a new workspace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,12 @@ import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/
import { CreateWorkspaceModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal'
import { InviteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal'
import { useSubscriptionData } from '@/hooks/queries/subscription'
import type { Workspace } from '@/hooks/queries/workspace'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'

const logger = createLogger('WorkspaceHeader')

interface Workspace {
id: string
name: string
color?: string
ownerId: string
role?: string
permissions?: 'admin' | 'write' | 'read' | null
}

interface WorkspaceHeaderProps {
/** The active workspace object */
activeWorkspace?: { name: string } | null
Expand All @@ -65,6 +57,8 @@ interface WorkspaceHeaderProps {
onRenameWorkspace: (workspaceId: string, newName: string) => Promise<void>
/** Callback to delete the workspace */
onDeleteWorkspace: (workspaceId: string) => Promise<void>
/** Whether workspace deletion is in progress */
isDeletingWorkspace: boolean
/** Callback to duplicate the workspace */
onDuplicateWorkspace: (workspaceId: string, workspaceName: string) => Promise<void>
/** Callback to export the workspace */
Expand All @@ -77,6 +71,8 @@ interface WorkspaceHeaderProps {
onColorChange?: (workspaceId: string, color: string) => Promise<void>
/** Callback to leave the workspace */
onLeaveWorkspace?: (workspaceId: string) => Promise<void>
/** Whether workspace leave is in progress */
isLeavingWorkspace: boolean
/** Current user's session ID for owner check */
sessionUserId?: string
/** Whether the sidebar is collapsed */
Expand All @@ -98,22 +94,22 @@ export function WorkspaceHeader({
onCreateWorkspace,
onRenameWorkspace,
onDeleteWorkspace,
isDeletingWorkspace,
onDuplicateWorkspace,
onExportWorkspace,
onImportWorkspace,
isImportingWorkspace,
onColorChange,
onLeaveWorkspace,
isLeavingWorkspace,
sessionUserId,
isCollapsed = false,
}: WorkspaceHeaderProps) {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<Workspace | null>(null)
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false)
const [isLeaving, setIsLeaving] = useState(false)
const [leaveTarget, setLeaveTarget] = useState<Workspace | null>(null)
const [editingWorkspaceId, setEditingWorkspaceId] = useState<string | null>(null)
const [editingName, setEditingName] = useState('')
Expand Down Expand Up @@ -296,32 +292,26 @@ export function WorkspaceHeader({
const handleLeaveWorkspace = async () => {
if (!leaveTarget || !onLeaveWorkspace) return

setIsLeaving(true)
try {
await onLeaveWorkspace(leaveTarget.id)
setIsLeaveModalOpen(false)
setLeaveTarget(null)
} catch (error) {
logger.error('Error leaving workspace:', error)
} finally {
setIsLeaving(false)
}
}

/**
* Handle delete workspace
* Handle delete workspace after confirmation
*/
const handleDeleteWorkspace = async () => {
setIsDeleting(true)
try {
const targetId = deleteTarget?.id || workspaceId
await onDeleteWorkspace(targetId)
setIsDeleteModalOpen(false)
setDeleteTarget(null)
} catch (error) {
logger.error('Error deleting workspace:', error)
} finally {
setIsDeleting(false)
}
}

Expand Down Expand Up @@ -638,7 +628,7 @@ export function WorkspaceHeader({
disableRename={!contextCanAdmin}
disableDuplicate={!contextCanEdit}
disableExport={!contextCanAdmin}
disableDelete={!contextCanAdmin}
disableDelete={!contextCanAdmin || workspaces.length <= 1}
disableColorChange={!contextCanAdmin}
/>
)
Expand Down Expand Up @@ -666,7 +656,7 @@ export function WorkspaceHeader({
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
onConfirm={handleDeleteWorkspace}
isDeleting={isDeleting}
isDeleting={isDeletingWorkspace}
itemType='workspace'
itemName={deleteTarget?.name || activeWorkspaceFull?.name || activeWorkspace?.name}
/>
Expand All @@ -686,12 +676,16 @@ export function WorkspaceHeader({
<Button
variant='default'
onClick={() => setIsLeaveModalOpen(false)}
disabled={isLeaving}
disabled={isLeavingWorkspace}
>
Cancel
</Button>
<Button variant='destructive' onClick={handleLeaveWorkspace} disabled={isLeaving}>
{isLeaving ? 'Leaving...' : 'Leave Workspace'}
<Button
variant='destructive'
onClick={handleLeaveWorkspace}
disabled={isLeavingWorkspace}
>
{isLeavingWorkspace ? 'Leaving...' : 'Leave Workspace'}
</Button>
</ModalFooter>
</ModalContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ export { useSidebarResize } from './use-sidebar-resize'
export { useTaskSelection } from './use-task-selection'
export { useWorkflowOperations } from './use-workflow-operations'
export { useWorkflowSelection } from './use-workflow-selection'
export { useWorkspaceManagement, type Workspace } from './use-workspace-management'
export { useWorkspaceManagement } from './use-workspace-management'
Loading
Loading