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
58 changes: 58 additions & 0 deletions apps/sim/app/api/folders/[id]/restore/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { captureServerEvent } from '@/lib/posthog/server'
import { performRestoreFolder } from '@/lib/workflows/orchestration/folder-lifecycle'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('RestoreFolderAPI')

export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id: folderId } = await params

try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json().catch(() => ({}))
const workspaceId = body.workspaceId as string | undefined

if (!workspaceId) {
return NextResponse.json({ error: 'Workspace ID is required' }, { status: 400 })
}

const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
if (permission !== 'admin' && permission !== 'write') {
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
}

const result = await performRestoreFolder({
folderId,
workspaceId,
userId: session.user.id,
})

if (!result.success) {
return NextResponse.json({ error: result.error }, { status: 400 })
}

logger.info(`Restored folder ${folderId}`, { restoredItems: result.restoredItems })

captureServerEvent(
session.user.id,
'folder_restored',
{ folder_id: folderId, workspace_id: workspaceId },
{ groups: { workspace: workspaceId } }
)

return NextResponse.json({ success: true, restoredItems: result.restoredItems })
} catch (error) {
logger.error(`Error restoring folder ${folderId}`, error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' },
{ status: 500 }
)
}
}
12 changes: 8 additions & 4 deletions apps/sim/app/api/folders/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { workflow, workflowFolder } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, asc, eq, isNull, min } from 'drizzle-orm'
import { and, asc, eq, isNotNull, isNull, min } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
Expand Down Expand Up @@ -47,12 +47,16 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Access denied to this workspace' }, { status: 403 })
}

// If user has workspace permissions, fetch ALL folders in the workspace
// This allows shared workspace members to see folders created by other users
const scope = searchParams.get('scope') ?? 'active'
const archivedFilter =
scope === 'archived'
? isNotNull(workflowFolder.archivedAt)
: isNull(workflowFolder.archivedAt)

const folders = await db
.select()
.from(workflowFolder)
.where(eq(workflowFolder.workspaceId, workspaceId))
.where(and(eq(workflowFolder.workspaceId, workspaceId), archivedFilter))
.orderBy(asc(workflowFolder.sortOrder), asc(workflowFolder.createdAt))

return NextResponse.json({ folders })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use client'

import { useMemo, useState } from 'react'
import { Search } from 'lucide-react'
import { Folder, Search } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Button, Combobox, SModalTabs, SModalTabsList, SModalTabsTrigger } from '@/components/emcn'
import { Input } from '@/components/ui'
import { formatDate } from '@/lib/core/utils/formatting'
import { RESOURCE_REGISTRY } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import type { MothershipResourceType } from '@/app/workspace/[workspaceId]/home/types'
import { DeletedItemSkeleton } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton'
import { useFolders, useRestoreFolder } from '@/hooks/queries/folders'
import { useKnowledgeBasesQuery, useRestoreKnowledgeBase } from '@/hooks/queries/kb/knowledge'
import { useRestoreTable, useTablesList } from '@/hooks/queries/tables'
import { useRestoreWorkflow, useWorkflows } from '@/hooks/queries/workflows'
Expand All @@ -29,10 +30,12 @@ function getResourceHref(
return `${base}/knowledge/${id}`
case 'file':
return `${base}/files`
case 'folder':
return `${base}/w`
}
}

type ResourceType = 'all' | 'workflow' | 'table' | 'knowledge' | 'file'
type ResourceType = 'all' | 'workflow' | 'table' | 'knowledge' | 'file' | 'folder'

type SortColumn = 'deleted' | 'name' | 'type'

Expand All @@ -51,7 +54,9 @@ const SORT_OPTIONS: { column: SortColumn; direction: 'asc' | 'desc'; label: stri

const ICON_CLASS = 'h-[14px] w-[14px]'

const RESOURCE_TYPE_TO_MOTHERSHIP: Record<Exclude<ResourceType, 'all'>, MothershipResourceType> = {
const RESOURCE_TYPE_TO_MOTHERSHIP: Partial<
Record<Exclude<ResourceType, 'all'>, MothershipResourceType>
> = {
workflow: 'workflow',
table: 'table',
knowledge: 'knowledgebase',
Expand All @@ -70,13 +75,15 @@ interface DeletedResource {
const TABS: { id: ResourceType; label: string }[] = [
{ id: 'all', label: 'All' },
{ id: 'workflow', label: 'Workflows' },
{ id: 'folder', label: 'Folders' },
{ id: 'table', label: 'Tables' },
{ id: 'knowledge', label: 'Knowledge Bases' },
{ id: 'file', label: 'Files' },
]

const TYPE_LABEL: Record<Exclude<ResourceType, 'all'>, string> = {
workflow: 'Workflow',
folder: 'Folder',
table: 'Table',
knowledge: 'Knowledge Base',
file: 'File',
Expand All @@ -97,7 +104,13 @@ function ResourceIcon({ resource }: { resource: DeletedResource }) {
)
}

if (resource.type === 'folder') {
const color = resource.color ?? '#6B7280'
return <Folder className={ICON_CLASS} style={{ color }} />
}

const mothershipType = RESOURCE_TYPE_TO_MOTHERSHIP[resource.type]
if (!mothershipType) return null
const config = RESOURCE_REGISTRY[mothershipType]
return (
<>
Expand All @@ -120,23 +133,30 @@ export function RecentlyDeleted() {
const [restoredItems, setRestoredItems] = useState<Map<string, DeletedResource>>(new Map())

const workflowsQuery = useWorkflows(workspaceId, { scope: 'archived' })
const foldersQuery = useFolders(workspaceId, { scope: 'archived' })
const tablesQuery = useTablesList(workspaceId, 'archived')
const knowledgeQuery = useKnowledgeBasesQuery(workspaceId, { scope: 'archived' })
const filesQuery = useWorkspaceFiles(workspaceId, 'archived')

const restoreWorkflow = useRestoreWorkflow()
const restoreFolder = useRestoreFolder()
const restoreTable = useRestoreTable()
const restoreKnowledgeBase = useRestoreKnowledgeBase()
const restoreWorkspaceFile = useRestoreWorkspaceFile()

const isLoading =
workflowsQuery.isLoading ||
foldersQuery.isLoading ||
tablesQuery.isLoading ||
knowledgeQuery.isLoading ||
filesQuery.isLoading

const error =
workflowsQuery.error || tablesQuery.error || knowledgeQuery.error || filesQuery.error
workflowsQuery.error ||
foldersQuery.error ||
tablesQuery.error ||
knowledgeQuery.error ||
filesQuery.error

const resources = useMemo<DeletedResource[]>(() => {
const items: DeletedResource[] = []
Expand All @@ -152,6 +172,17 @@ export function RecentlyDeleted() {
})
}

for (const folder of foldersQuery.data ?? []) {
items.push({
id: folder.id,
name: folder.name,
type: 'folder',
deletedAt: folder.archivedAt ? new Date(folder.archivedAt) : new Date(folder.updatedAt),
workspaceId: folder.workspaceId,
color: folder.color,
})
}

for (const t of tablesQuery.data ?? []) {
items.push({
id: t.id,
Expand Down Expand Up @@ -193,6 +224,7 @@ export function RecentlyDeleted() {
return items
}, [
workflowsQuery.data,
foldersQuery.data,
tablesQuery.data,
knowledgeQuery.data,
filesQuery.data,
Expand Down Expand Up @@ -250,6 +282,12 @@ export function RecentlyDeleted() {
{ onSettled, onSuccess }
)
break
case 'folder':
restoreFolder.mutate(
{ folderId: resource.id, workspaceId: resource.workspaceId },
{ onSettled, onSuccess }
)
break
case 'table':
restoreTable.mutate(resource.id, { onSettled, onSuccess })
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function DeleteModal({
title = 'Delete Workspace'
}

const restorableTypes = new Set<string>(['workflow'])
const restorableTypes = new Set<string>(['workflow', 'folder', 'mixed'])

const renderDescription = () => {
if (itemType === 'workflow') {
Expand Down Expand Up @@ -113,8 +113,7 @@ export function DeleteModal({
</span>
?{' '}
<span className='text-[var(--text-error)]'>
This will permanently remove all workflows, logs, and knowledge bases within these
folders.
All workflows and contents within these folders will be archived.
</span>
</>
)
Expand All @@ -125,7 +124,7 @@ export function DeleteModal({
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{displayNames[0]}</span>?{' '}
<span className='text-[var(--text-error)]'>
This will permanently remove all associated workflows, logs, and knowledge bases.
All associated workflows and contents will be archived.
</span>
</>
)
Expand All @@ -134,7 +133,7 @@ export function DeleteModal({
<>
Are you sure you want to delete this folder?{' '}
<span className='text-[var(--text-error)]'>
This will permanently remove all associated workflows, logs, and knowledge bases.
All associated workflows and contents will be archived.
</span>
</>
)
Expand Down Expand Up @@ -186,8 +185,7 @@ export function DeleteModal({
</span>
?{' '}
<span className='text-[var(--text-error)]'>
This will permanently remove all selected workflows and folders, including their
contents.
All selected workflows and folders, including their contents, will be archived.
</span>
</>
)
Expand All @@ -196,8 +194,7 @@ export function DeleteModal({
<>
Are you sure you want to delete the selected items?{' '}
<span className='text-[var(--text-error)]'>
This will permanently remove all selected workflows and folders, including their
contents.
All selected workflows and folders, including their contents, will be archived.
</span>
</>
)
Expand Down Expand Up @@ -238,7 +235,7 @@ export function DeleteModal({
You can restore it from Recently Deleted in Settings.
</span>
) : (
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
<span className='text-[var(--text-tertiary)]'>This action cannot be undone.</span>
)}
</p>
</ModalBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
{memberToRemove?.email}
</span>{' '}
from this workspace?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
<span className='text-[var(--text-tertiary)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
Expand Down Expand Up @@ -646,7 +646,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
<span className='font-medium text-[var(--text-primary)]'>
{invitationToRemove?.email}
</span>
? <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
? <span className='text-[var(--text-tertiary)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ export function WorkspaceHeader({
Are you sure you want to leave{' '}
<span className='font-base text-[var(--text-primary)]'>{leaveTarget?.name}</span>? You
will lose access to all workflows and data in this workspace.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
<span className='text-[var(--text-tertiary)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/ee/access-control/components/access-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1290,7 +1290,7 @@ export function AccessControl() {
<span className='text-[var(--text-error)]'>
All members will be removed from this group.
</span>{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
<span className='text-[var(--text-tertiary)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
Expand Down
Loading
Loading