diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index 91b267d72d9..3de43832e54 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -666,39 +666,7 @@ input[type="search"]::-ms-clear { } } -/** - * Notification toast enter animation — pop-open with stack offset - */ -@keyframes notification-enter { - from { - opacity: 0; - transform: translateX(calc(var(--stack-offset, 0px) - 8px)) scale(0.97); - } - to { - opacity: 1; - transform: translateX(var(--stack-offset, 0px)) scale(1); - } -} - -@keyframes notification-countdown { - from { - stroke-dashoffset: 0; - } - to { - stroke-dashoffset: 34.56; - } -} -@keyframes notification-exit { - from { - opacity: 1; - transform: translateX(var(--stack-offset, 0px)) scale(1); - } - to { - opacity: 0; - transform: translateX(calc(var(--stack-offset, 0px) + 8px)) scale(0.97); - } -} /** * @depricated diff --git a/apps/sim/app/api/knowledge/[id]/restore/route.ts b/apps/sim/app/api/knowledge/[id]/restore/route.ts new file mode 100644 index 00000000000..f247d6aa276 --- /dev/null +++ b/apps/sim/app/api/knowledge/[id]/restore/route.ts @@ -0,0 +1,61 @@ +import { db } from '@sim/db' +import { knowledgeBase } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { restoreKnowledgeBase } from '@/lib/knowledge/service' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('RestoreKnowledgeBaseAPI') + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const requestId = generateRequestId() + const { id } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const [kb] = await db + .select({ + id: knowledgeBase.id, + workspaceId: knowledgeBase.workspaceId, + userId: knowledgeBase.userId, + }) + .from(knowledgeBase) + .where(eq(knowledgeBase.id, id)) + .limit(1) + + if (!kb) { + return NextResponse.json({ error: 'Knowledge base not found' }, { status: 404 }) + } + + if (kb.workspaceId) { + const permission = await getUserEntityPermissions(auth.userId, 'workspace', kb.workspaceId) + if (permission !== 'admin' && permission !== 'write') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + } else if (kb.userId !== auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + await restoreKnowledgeBase(id, requestId) + + logger.info(`[${requestId}] Restored knowledge base ${id}`) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error(`[${requestId}] Error restoring knowledge base ${id}`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/table/[tableId]/restore/route.ts b/apps/sim/app/api/table/[tableId]/restore/route.ts new file mode 100644 index 00000000000..8622f849f1d --- /dev/null +++ b/apps/sim/app/api/table/[tableId]/restore/route.ts @@ -0,0 +1,45 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { getTableById, restoreTable } from '@/lib/table' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('RestoreTableAPI') + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ tableId: string }> } +) { + const requestId = generateRequestId() + const { tableId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) + } + + const table = await getTableById(tableId, { includeArchived: true }) + if (!table) { + return NextResponse.json({ error: 'Table not found' }, { status: 404 }) + } + + const permission = await getUserEntityPermissions(auth.userId, 'workspace', table.workspaceId) + if (permission !== 'admin' && permission !== 'write') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + await restoreTable(tableId, requestId) + + logger.info(`[${requestId}] Restored table ${tableId}`) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error(`[${requestId}] Error restoring table ${tableId}`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/workflows/[id]/restore/route.ts b/apps/sim/app/api/workflows/[id]/restore/route.ts new file mode 100644 index 00000000000..7e8a76e8a35 --- /dev/null +++ b/apps/sim/app/api/workflows/[id]/restore/route.ts @@ -0,0 +1,55 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { restoreWorkflow } from '@/lib/workflows/lifecycle' +import { getWorkflowById } from '@/lib/workflows/utils' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('RestoreWorkflowAPI') + +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = generateRequestId() + const { id: workflowId } = await params + + try { + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const workflowData = await getWorkflowById(workflowId, { includeArchived: true }) + if (!workflowData) { + return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) + } + + if (workflowData.workspaceId) { + const permission = await getUserEntityPermissions( + auth.userId, + 'workspace', + workflowData.workspaceId + ) + if (permission !== 'admin' && permission !== 'write') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + } else if (workflowData.userId !== auth.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const result = await restoreWorkflow(workflowId, { requestId }) + + if (!result.restored) { + return NextResponse.json({ error: 'Workflow is not archived' }, { status: 400 }) + } + + logger.info(`[${requestId}] Restored workflow ${workflowId}`) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error(`[${requestId}] Error restoring workflow ${workflowId}`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/restore/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/restore/route.ts new file mode 100644 index 00000000000..eae4bae4368 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/restore/route.ts @@ -0,0 +1,40 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { restoreWorkspaceFile } from '@/lib/uploads/contexts/workspace' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('RestoreWorkspaceFileAPI') + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string; fileId: string }> } +) { + const requestId = generateRequestId() + const { id: workspaceId, fileId } = await params + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (userPermission !== 'admin' && userPermission !== 'write') { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }) + } + + await restoreWorkspaceFile(workspaceId, fileId) + + logger.info(`[${requestId}] Restored workspace file ${fileId}`) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error(`[${requestId}] Error restoring workspace file ${fileId}`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 8cbc8014fc3..328c2b265b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -792,7 +792,9 @@ function DeleteConfirmModal({
Are you sure you want to delete{' '} {fileName}?{' '} - This action cannot be undone. + + You can restore it from Recently Deleted in Settings. +
Are you sure you want to delete{' '} {knowledgeBaseName}? - This will permanently delete the knowledge base and all {pagination.total} document - {pagination.total === 1 ? '' : 's'} within it.{' '} - This action cannot be undone. + The knowledge base and all {pagination.total} document + {pagination.total === 1 ? '' : 's'} within it will be removed.{' '} + + You can restore it from Recently Deleted in Settings. +
+ Items you delete are kept here for 30 days before being permanently removed. +
+ ++ {showNoResults + ? `No items found matching \u201c${searchTerm}\u201d` + : 'No deleted items'} +
+Are you sure you want to delete{' '} {tableData?.name}?{' '} - This action cannot be undone. + + You can restore it from Recently Deleted in Settings. +
Are you sure you want to delete{' '} {activeTable?.name}? - This will permanently delete all {activeTable?.rowCount} rows.{' '} - This action cannot be undone. + All {activeTable?.rowCount} rows will be removed.{' '} + + You can restore it from Recently Deleted in Settings. +
Keep visible
-{renderDescription()}{' '} - This action cannot be undone. + {restorableTypes.has(itemType) ? ( + + You can restore it from Recently Deleted in Settings. + + ) : ( + This action cannot be undone. + )}
Keep visible
+