Skip to content

Commit 684a802

Browse files
committed
Closer
1 parent bacb6f3 commit 684a802

File tree

3 files changed

+400
-121
lines changed

3 files changed

+400
-121
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { eq } from 'drizzle-orm'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { z } from 'zod'
4+
import { getSession } from '@/lib/auth'
5+
import { createLogger } from '@/lib/logs/console-logger'
6+
import { getUserEntityPermissions } from '@/lib/permissions/utils'
7+
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
8+
import { db } from '@/db'
9+
import { workflow } from '@/db/schema'
10+
11+
const logger = createLogger('WorkflowStateAPI')
12+
13+
const WorkflowStateSchema = z.object({
14+
blocks: z.record(z.any()),
15+
edges: z.array(z.any()),
16+
loops: z.record(z.any()).optional(),
17+
parallels: z.record(z.any()).optional(),
18+
lastSaved: z.number().optional(),
19+
isDeployed: z.boolean().optional(),
20+
deployedAt: z.date().optional(),
21+
deploymentStatuses: z.record(z.any()).optional(),
22+
hasActiveSchedule: z.boolean().optional(),
23+
hasActiveWebhook: z.boolean().optional(),
24+
})
25+
26+
/**
27+
* PUT /api/workflows/[id]/state
28+
* Save complete workflow state to normalized database tables
29+
*/
30+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
31+
const requestId = crypto.randomUUID().slice(0, 8)
32+
const startTime = Date.now()
33+
const { id: workflowId } = await params
34+
35+
try {
36+
// Get the session
37+
const session = await getSession()
38+
if (!session?.user?.id) {
39+
logger.warn(`[${requestId}] Unauthorized state update attempt for workflow ${workflowId}`)
40+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
41+
}
42+
43+
const userId = session.user.id
44+
45+
// Parse and validate request body
46+
const body = await request.json()
47+
const state = WorkflowStateSchema.parse(body)
48+
49+
// Fetch the workflow to check ownership/access
50+
const workflowData = await db
51+
.select()
52+
.from(workflow)
53+
.where(eq(workflow.id, workflowId))
54+
.then((rows) => rows[0])
55+
56+
if (!workflowData) {
57+
logger.warn(`[${requestId}] Workflow ${workflowId} not found for state update`)
58+
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
59+
}
60+
61+
// Check if user has permission to update this workflow
62+
let canUpdate = false
63+
64+
// Case 1: User owns the workflow
65+
if (workflowData.userId === userId) {
66+
canUpdate = true
67+
}
68+
69+
// Case 2: Workflow belongs to a workspace and user has write or admin permission
70+
if (!canUpdate && workflowData.workspaceId) {
71+
const userPermission = await getUserEntityPermissions(
72+
userId,
73+
'workspace',
74+
workflowData.workspaceId
75+
)
76+
if (userPermission === 'write' || userPermission === 'admin') {
77+
canUpdate = true
78+
}
79+
}
80+
81+
if (!canUpdate) {
82+
logger.warn(
83+
`[${requestId}] User ${userId} denied permission to update workflow state ${workflowId}`
84+
)
85+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
86+
}
87+
88+
// Save to normalized tables
89+
logger.info(`[${requestId}] Saving workflow ${workflowId} state to normalized tables`)
90+
91+
// Ensure all required fields are present for WorkflowState type
92+
const workflowState = {
93+
blocks: state.blocks,
94+
edges: state.edges,
95+
loops: state.loops || {},
96+
parallels: state.parallels || {},
97+
lastSaved: state.lastSaved || Date.now(),
98+
isDeployed: state.isDeployed || false,
99+
deployedAt: state.deployedAt,
100+
deploymentStatuses: state.deploymentStatuses || {},
101+
hasActiveSchedule: state.hasActiveSchedule || false,
102+
hasActiveWebhook: state.hasActiveWebhook || false,
103+
}
104+
105+
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)
106+
107+
if (!saveResult.success) {
108+
logger.error(`[${requestId}] Failed to save workflow ${workflowId} state:`, saveResult.error)
109+
return NextResponse.json(
110+
{ error: 'Failed to save workflow state', details: saveResult.error },
111+
{ status: 500 }
112+
)
113+
}
114+
115+
// Update workflow's lastSynced timestamp
116+
await db
117+
.update(workflow)
118+
.set({
119+
lastSynced: new Date(),
120+
updatedAt: new Date(),
121+
state: saveResult.jsonBlob // Also update JSON blob for backward compatibility
122+
})
123+
.where(eq(workflow.id, workflowId))
124+
125+
const elapsed = Date.now() - startTime
126+
logger.info(`[${requestId}] Successfully saved workflow ${workflowId} state in ${elapsed}ms`)
127+
128+
return NextResponse.json({
129+
success: true,
130+
blocksCount: Object.keys(state.blocks).length,
131+
edgesCount: state.edges.length
132+
}, { status: 200 })
133+
134+
} catch (error: any) {
135+
const elapsed = Date.now() - startTime
136+
if (error instanceof z.ZodError) {
137+
logger.warn(`[${requestId}] Invalid workflow state data for ${workflowId}`, {
138+
errors: error.errors,
139+
})
140+
return NextResponse.json(
141+
{ error: 'Invalid state data', details: error.errors },
142+
{ status: 400 }
143+
)
144+
}
145+
146+
logger.error(`[${requestId}] Error saving workflow ${workflowId} state after ${elapsed}ms`, error)
147+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
148+
}
149+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/import-controls/import-controls.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
5151

5252
// Stores and hooks
5353
const { createWorkflow } = useWorkflowRegistry()
54-
const { collaborativeAddBlock, collaborativeAddEdge } = useCollaborativeWorkflow()
54+
const {
55+
collaborativeAddBlock,
56+
collaborativeAddEdge,
57+
collaborativeSetSubblockValue
58+
} = useCollaborativeWorkflow()
5559
const subBlockStore = useSubBlockStore()
5660

5761
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -116,10 +120,10 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
116120
// Navigate to the new workflow
117121
router.push(`/workspace/${workspaceId}/w/${newWorkflowId}`)
118122

119-
// Small delay to ensure navigation and workflow initialization
120-
await new Promise((resolve) => setTimeout(resolve, 1000))
123+
// Brief delay to ensure navigation completes
124+
await new Promise((resolve) => setTimeout(resolve, 100))
121125

122-
// Import the YAML into the new workflow
126+
// Import the YAML into the new workflow (creates complete state and saves directly to DB)
123127
const result = await importWorkflowFromYaml(yamlContent, {
124128
addBlock: collaborativeAddBlock,
125129
addEdge: collaborativeAddEdge,
@@ -128,7 +132,8 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
128132
window.dispatchEvent(new CustomEvent('trigger-auto-layout'))
129133
},
130134
setSubBlockValue: (blockId: string, subBlockId: string, value: any) => {
131-
subBlockStore.setValue(blockId, subBlockId, value)
135+
// Use the collaborative function - the same one called when users type into fields
136+
collaborativeSetSubblockValue(blockId, subBlockId, value)
132137
},
133138
getExistingBlocks: () => {
134139
// This will be called after navigation, so we need to get blocks from the store
@@ -140,12 +145,9 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
140145
setImportResult(result)
141146

142147
if (result.success) {
143-
// Close dialog on success
144-
setTimeout(() => {
145-
setShowYamlDialog(false)
146-
setYamlContent('')
147-
setImportResult(null)
148-
}, 2000)
148+
setYamlContent('')
149+
setShowYamlDialog(false)
150+
logger.info('YAML import completed successfully')
149151
}
150152
} catch (error) {
151153
logger.error('Failed to import YAML workflow:', error)

0 commit comments

Comments
 (0)