Skip to content

Commit f5ae468

Browse files
TheodoreSpeaksTheodore Li
andauthored
feat(restore) Add restore endpoints and ui (#3570)
* Add restore endpoints and ui * Derive toast from notification * Auth user if workspaceid not found * Fix recently deleted ui * Add restore error toast * Fix deleted at timestamp mismatch --------- Co-authored-by: Theodore Li <theo@sim.ai>
1 parent 22c2571 commit f5ae468

File tree

32 files changed

+1357
-299
lines changed

32 files changed

+1357
-299
lines changed

apps/sim/app/_styles/globals.css

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -666,39 +666,7 @@ input[type="search"]::-ms-clear {
666666
}
667667
}
668668

669-
/**
670-
* Notification toast enter animation — pop-open with stack offset
671-
*/
672-
@keyframes notification-enter {
673-
from {
674-
opacity: 0;
675-
transform: translateX(calc(var(--stack-offset, 0px) - 8px)) scale(0.97);
676-
}
677-
to {
678-
opacity: 1;
679-
transform: translateX(var(--stack-offset, 0px)) scale(1);
680-
}
681-
}
682-
683-
@keyframes notification-countdown {
684-
from {
685-
stroke-dashoffset: 0;
686-
}
687-
to {
688-
stroke-dashoffset: 34.56;
689-
}
690-
}
691669

692-
@keyframes notification-exit {
693-
from {
694-
opacity: 1;
695-
transform: translateX(var(--stack-offset, 0px)) scale(1);
696-
}
697-
to {
698-
opacity: 0;
699-
transform: translateX(calc(var(--stack-offset, 0px) + 8px)) scale(0.97);
700-
}
701-
}
702670

703671
/**
704672
* @depricated
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { db } from '@sim/db'
2+
import { knowledgeBase } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { eq } from 'drizzle-orm'
5+
import { type NextRequest, NextResponse } from 'next/server'
6+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
7+
import { generateRequestId } from '@/lib/core/utils/request'
8+
import { restoreKnowledgeBase } from '@/lib/knowledge/service'
9+
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
10+
11+
const logger = createLogger('RestoreKnowledgeBaseAPI')
12+
13+
export async function POST(
14+
request: NextRequest,
15+
{ params }: { params: Promise<{ id: string }> }
16+
) {
17+
const requestId = generateRequestId()
18+
const { id } = await params
19+
20+
try {
21+
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
22+
if (!auth.success || !auth.userId) {
23+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
24+
}
25+
26+
const [kb] = await db
27+
.select({
28+
id: knowledgeBase.id,
29+
workspaceId: knowledgeBase.workspaceId,
30+
userId: knowledgeBase.userId,
31+
})
32+
.from(knowledgeBase)
33+
.where(eq(knowledgeBase.id, id))
34+
.limit(1)
35+
36+
if (!kb) {
37+
return NextResponse.json({ error: 'Knowledge base not found' }, { status: 404 })
38+
}
39+
40+
if (kb.workspaceId) {
41+
const permission = await getUserEntityPermissions(auth.userId, 'workspace', kb.workspaceId)
42+
if (permission !== 'admin' && permission !== 'write') {
43+
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
44+
}
45+
} else if (kb.userId !== auth.userId) {
46+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
47+
}
48+
49+
await restoreKnowledgeBase(id, requestId)
50+
51+
logger.info(`[${requestId}] Restored knowledge base ${id}`)
52+
53+
return NextResponse.json({ success: true })
54+
} catch (error) {
55+
logger.error(`[${requestId}] Error restoring knowledge base ${id}`, error)
56+
return NextResponse.json(
57+
{ error: error instanceof Error ? error.message : 'Internal server error' },
58+
{ status: 500 }
59+
)
60+
}
61+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
4+
import { generateRequestId } from '@/lib/core/utils/request'
5+
import { getTableById, restoreTable } from '@/lib/table'
6+
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
7+
8+
const logger = createLogger('RestoreTableAPI')
9+
10+
export async function POST(
11+
request: NextRequest,
12+
{ params }: { params: Promise<{ tableId: string }> }
13+
) {
14+
const requestId = generateRequestId()
15+
const { tableId } = await params
16+
17+
try {
18+
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
19+
if (!auth.success || !auth.userId) {
20+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
21+
}
22+
23+
const table = await getTableById(tableId, { includeArchived: true })
24+
if (!table) {
25+
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
26+
}
27+
28+
const permission = await getUserEntityPermissions(auth.userId, 'workspace', table.workspaceId)
29+
if (permission !== 'admin' && permission !== 'write') {
30+
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
31+
}
32+
33+
await restoreTable(tableId, requestId)
34+
35+
logger.info(`[${requestId}] Restored table ${tableId}`)
36+
37+
return NextResponse.json({ success: true })
38+
} catch (error) {
39+
logger.error(`[${requestId}] Error restoring table ${tableId}`, error)
40+
return NextResponse.json(
41+
{ error: error instanceof Error ? error.message : 'Internal server error' },
42+
{ status: 500 }
43+
)
44+
}
45+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
4+
import { generateRequestId } from '@/lib/core/utils/request'
5+
import { restoreWorkflow } from '@/lib/workflows/lifecycle'
6+
import { getWorkflowById } from '@/lib/workflows/utils'
7+
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
8+
9+
const logger = createLogger('RestoreWorkflowAPI')
10+
11+
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
12+
const requestId = generateRequestId()
13+
const { id: workflowId } = await params
14+
15+
try {
16+
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
17+
if (!auth.success || !auth.userId) {
18+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
19+
}
20+
21+
const workflowData = await getWorkflowById(workflowId, { includeArchived: true })
22+
if (!workflowData) {
23+
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
24+
}
25+
26+
if (workflowData.workspaceId) {
27+
const permission = await getUserEntityPermissions(
28+
auth.userId,
29+
'workspace',
30+
workflowData.workspaceId
31+
)
32+
if (permission !== 'admin' && permission !== 'write') {
33+
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
34+
}
35+
} else if (workflowData.userId !== auth.userId) {
36+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
37+
}
38+
39+
const result = await restoreWorkflow(workflowId, { requestId })
40+
41+
if (!result.restored) {
42+
return NextResponse.json({ error: 'Workflow is not archived' }, { status: 400 })
43+
}
44+
45+
logger.info(`[${requestId}] Restored workflow ${workflowId}`)
46+
47+
return NextResponse.json({ success: true })
48+
} catch (error) {
49+
logger.error(`[${requestId}] Error restoring workflow ${workflowId}`, error)
50+
return NextResponse.json(
51+
{ error: error instanceof Error ? error.message : 'Internal server error' },
52+
{ status: 500 }
53+
)
54+
}
55+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { getSession } from '@/lib/auth'
4+
import { generateRequestId } from '@/lib/core/utils/request'
5+
import { restoreWorkspaceFile } from '@/lib/uploads/contexts/workspace'
6+
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
7+
8+
const logger = createLogger('RestoreWorkspaceFileAPI')
9+
10+
export async function POST(
11+
request: NextRequest,
12+
{ params }: { params: Promise<{ id: string; fileId: string }> }
13+
) {
14+
const requestId = generateRequestId()
15+
const { id: workspaceId, fileId } = await params
16+
17+
try {
18+
const session = await getSession()
19+
if (!session?.user?.id) {
20+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
21+
}
22+
23+
const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
24+
if (userPermission !== 'admin' && userPermission !== 'write') {
25+
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
26+
}
27+
28+
await restoreWorkspaceFile(workspaceId, fileId)
29+
30+
logger.info(`[${requestId}] Restored workspace file ${fileId}`)
31+
32+
return NextResponse.json({ success: true })
33+
} catch (error) {
34+
logger.error(`[${requestId}] Error restoring workspace file ${fileId}`, error)
35+
return NextResponse.json(
36+
{ error: error instanceof Error ? error.message : 'Internal server error' },
37+
{ status: 500 }
38+
)
39+
}
40+
}

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,9 @@ function DeleteConfirmModal({
792792
<p className='text-[var(--text-secondary)]'>
793793
Are you sure you want to delete{' '}
794794
<span className='font-medium text-[var(--text-primary)]'>{fileName}</span>?{' '}
795-
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
795+
<span className='text-[var(--text-tertiary)]'>
796+
You can restore it from Recently Deleted in Settings.
797+
</span>
796798
</p>
797799
</ModalBody>
798800
<ModalFooter>

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,9 +1107,11 @@ export function KnowledgeBase({
11071107
<p className='text-[var(--text-secondary)]'>
11081108
Are you sure you want to delete{' '}
11091109
<span className='font-medium text-[var(--text-primary)]'>{knowledgeBaseName}</span>?
1110-
This will permanently delete the knowledge base and all {pagination.total} document
1111-
{pagination.total === 1 ? '' : 's'} within it.{' '}
1112-
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
1110+
The knowledge base and all {pagination.total} document
1111+
{pagination.total === 1 ? '' : 's'} within it will be removed.{' '}
1112+
<span className='text-[var(--text-tertiary)]'>
1113+
You can restore it from Recently Deleted in Settings.
1114+
</span>
11131115
</p>
11141116
</ModalBody>
11151117
<ModalFooter>

apps/sim/app/workspace/[workspaceId]/knowledge/components/delete-knowledge-base-modal/delete-knowledge-base-modal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ export function DeleteKnowledgeBaseModal({
4646
<>
4747
Are you sure you want to delete{' '}
4848
<span className='font-medium text-[var(--text-primary)]'>{knowledgeBaseName}</span>?
49-
This will permanently remove all associated documents, chunks, and embeddings.
49+
All associated documents, chunks, and embeddings will be removed.
5050
</>
5151
) : (
52-
'Are you sure you want to delete this knowledge base? This will permanently remove all associated documents, chunks, and embeddings.'
52+
'Are you sure you want to delete this knowledge base? All associated documents, chunks, and embeddings will be removed.'
5353
)}{' '}
54-
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
54+
<span className='text-[var(--text-tertiary)]'>
55+
You can restore it from Recently Deleted in Settings.
56+
</span>
5557
</p>
5658
</ModalBody>
5759
<ModalFooter>

apps/sim/app/workspace/[workspaceId]/layout.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { ToastProvider } from '@/components/emcn'
34
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
45
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
56
import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader'
@@ -8,7 +9,7 @@ import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/side
89

910
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
1011
return (
11-
<>
12+
<ToastProvider>
1213
<SettingsLoader />
1314
<ProviderModelsLoader />
1415
<GlobalCommandsProvider>
@@ -25,6 +26,6 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
2526
</WorkspacePermissionsProvider>
2627
</div>
2728
</GlobalCommandsProvider>
28-
</>
29+
</ToastProvider>
2930
)
3031
}

apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const SECTION_TITLES: Record<string, string> = {
2222
skills: 'Skills',
2323
'workflow-mcp-servers': 'MCP Servers',
2424
'credential-sets': 'Email Polling',
25+
'recently-deleted': 'Recently Deleted',
2526
debug: 'Debug',
2627
} as const
2728

0 commit comments

Comments
 (0)