Skip to content

Commit 7022b4c

Browse files
committed
types improvement, preview fixes
1 parent ea8c710 commit 7022b4c

File tree

52 files changed

+1905
-922
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1905
-922
lines changed

apps/sim/app/api/v1/admin/types.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export interface WorkflowExportState {
243243
color?: string
244244
exportedAt?: string
245245
}
246-
variables?: WorkflowVariable[]
246+
variables?: Record<string, WorkflowVariable>
247247
}
248248

249249
export interface WorkflowExportPayload {
@@ -317,36 +317,44 @@ export interface WorkspaceImportResponse {
317317
// =============================================================================
318318

319319
/**
320-
* Parse workflow variables from database JSON format to array format.
321-
* Handles both array and Record<string, Variable> formats.
320+
* Parse workflow variables from database JSON format to Record format.
321+
* Handles both legacy Array and current Record<string, Variable> formats.
322322
*/
323323
export function parseWorkflowVariables(
324324
dbVariables: DbWorkflow['variables']
325-
): WorkflowVariable[] | undefined {
325+
): Record<string, WorkflowVariable> | undefined {
326326
if (!dbVariables) return undefined
327327

328328
try {
329329
const varsObj = typeof dbVariables === 'string' ? JSON.parse(dbVariables) : dbVariables
330330

331+
// Handle legacy Array format by converting to Record
331332
if (Array.isArray(varsObj)) {
332-
return varsObj.map((v) => ({
333-
id: v.id,
334-
name: v.name,
335-
type: v.type,
336-
value: v.value,
337-
}))
333+
const result: Record<string, WorkflowVariable> = {}
334+
for (const v of varsObj) {
335+
result[v.id] = {
336+
id: v.id,
337+
name: v.name,
338+
type: v.type,
339+
value: v.value,
340+
}
341+
}
342+
return result
338343
}
339344

345+
// Already Record format - normalize and return
340346
if (typeof varsObj === 'object' && varsObj !== null) {
341-
return Object.values(varsObj).map((v: unknown) => {
347+
const result: Record<string, WorkflowVariable> = {}
348+
for (const [key, v] of Object.entries(varsObj)) {
342349
const variable = v as { id: string; name: string; type: VariableType; value: unknown }
343-
return {
350+
result[key] = {
344351
id: variable.id,
345352
name: variable.name,
346353
type: variable.type,
347354
value: variable.value,
348355
}
349-
})
356+
}
357+
return result
350358
}
351359
} catch {
352360
// pass

apps/sim/app/api/workflows/[id]/variables/route.test.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,15 @@ describe('Workflow Variables API Route', () => {
207207
update: { results: [{}] },
208208
})
209209

210-
const variables = [
211-
{ id: 'var-1', workflowId: 'workflow-123', name: 'test', type: 'string', value: 'hello' },
212-
]
210+
const variables = {
211+
'var-1': {
212+
id: 'var-1',
213+
workflowId: 'workflow-123',
214+
name: 'test',
215+
type: 'string',
216+
value: 'hello',
217+
},
218+
}
213219

214220
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables', {
215221
method: 'POST',
@@ -242,9 +248,15 @@ describe('Workflow Variables API Route', () => {
242248
isWorkspaceOwner: false,
243249
})
244250

245-
const variables = [
246-
{ id: 'var-1', workflowId: 'workflow-123', name: 'test', type: 'string', value: 'hello' },
247-
]
251+
const variables = {
252+
'var-1': {
253+
id: 'var-1',
254+
workflowId: 'workflow-123',
255+
name: 'test',
256+
type: 'string',
257+
value: 'hello',
258+
},
259+
}
248260

249261
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables', {
250262
method: 'POST',
@@ -277,7 +289,6 @@ describe('Workflow Variables API Route', () => {
277289
isWorkspaceOwner: false,
278290
})
279291

280-
// Invalid data - missing required fields
281292
const invalidData = { variables: [{ name: 'test' }] }
282293

283294
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables', {

apps/sim/app/api/workflows/[id]/variables/route.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,22 @@ import type { Variable } from '@/stores/panel/variables/types'
1111

1212
const logger = createLogger('WorkflowVariablesAPI')
1313

14+
const VariableSchema = z.object({
15+
id: z.string(),
16+
workflowId: z.string(),
17+
name: z.string(),
18+
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'plain']),
19+
value: z.union([
20+
z.string(),
21+
z.number(),
22+
z.boolean(),
23+
z.record(z.unknown()),
24+
z.array(z.unknown()),
25+
]),
26+
})
27+
1428
const VariablesSchema = z.object({
15-
variables: z.array(
16-
z.object({
17-
id: z.string(),
18-
workflowId: z.string(),
19-
name: z.string(),
20-
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'plain']),
21-
value: z.union([z.string(), z.number(), z.boolean(), z.record(z.any()), z.array(z.any())]),
22-
})
23-
),
29+
variables: z.record(z.string(), VariableSchema),
2430
})
2531

2632
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
@@ -60,21 +66,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
6066
try {
6167
const { variables } = VariablesSchema.parse(body)
6268

63-
// Format variables for storage
64-
const variablesRecord: Record<string, Variable> = {}
65-
variables.forEach((variable) => {
66-
variablesRecord[variable.id] = variable
67-
})
68-
69-
// Replace variables completely with the incoming ones
69+
// Variables are already in Record format - use directly
7070
// The frontend is the source of truth for what variables should exist
71-
const updatedVariables = variablesRecord
72-
73-
// Update workflow with variables
7471
await db
7572
.update(workflow)
7673
.set({
77-
variables: updatedVariables,
74+
variables,
7875
updatedAt: new Date(),
7976
})
8077
.where(eq(workflow.id, workflowId))
@@ -148,8 +145,9 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
148145
headers,
149146
}
150147
)
151-
} catch (error: any) {
148+
} catch (error) {
152149
logger.error(`[${requestId}] Workflow variables fetch error`, error)
153-
return NextResponse.json({ error: error.message }, { status: 500 })
150+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
151+
return NextResponse.json({ error: errorMessage }, { status: 500 })
154152
}
155153
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SnapshotContextMenu } from './snapshot-context-menu'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use client'
2+
3+
import type { RefObject } from 'react'
4+
import { createPortal } from 'react-dom'
5+
import {
6+
Popover,
7+
PopoverAnchor,
8+
PopoverContent,
9+
PopoverDivider,
10+
PopoverItem,
11+
} from '@/components/emcn'
12+
13+
interface SnapshotContextMenuProps {
14+
isOpen: boolean
15+
position: { x: number; y: number }
16+
menuRef: RefObject<HTMLDivElement | null>
17+
onClose: () => void
18+
onCopy: () => void
19+
onSearch?: () => void
20+
wrapText?: boolean
21+
onToggleWrap?: () => void
22+
/** When true, only shows Copy option (for subblock values) */
23+
copyOnly?: boolean
24+
}
25+
26+
/**
27+
* Context menu for execution snapshot sidebar.
28+
* Provides copy, search, and display options.
29+
* Uses createPortal to render outside any transformed containers (like modals).
30+
*/
31+
export function SnapshotContextMenu({
32+
isOpen,
33+
position,
34+
menuRef,
35+
onClose,
36+
onCopy,
37+
onSearch,
38+
wrapText,
39+
onToggleWrap,
40+
copyOnly = false,
41+
}: SnapshotContextMenuProps) {
42+
if (typeof document === 'undefined') return null
43+
44+
return createPortal(
45+
<Popover
46+
open={isOpen}
47+
onOpenChange={onClose}
48+
variant='secondary'
49+
size='sm'
50+
colorScheme='inverted'
51+
>
52+
<PopoverAnchor
53+
style={{
54+
position: 'fixed',
55+
left: `${position.x}px`,
56+
top: `${position.y}px`,
57+
width: '1px',
58+
height: '1px',
59+
}}
60+
/>
61+
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
62+
<PopoverItem
63+
onClick={() => {
64+
onCopy()
65+
onClose()
66+
}}
67+
>
68+
Copy
69+
</PopoverItem>
70+
71+
{!copyOnly && onSearch && (
72+
<>
73+
<PopoverDivider />
74+
<PopoverItem
75+
onClick={() => {
76+
onSearch()
77+
onClose()
78+
}}
79+
>
80+
Search
81+
</PopoverItem>
82+
</>
83+
)}
84+
85+
{!copyOnly && onToggleWrap && (
86+
<>
87+
<PopoverDivider />
88+
<PopoverItem showCheck={wrapText} onClick={onToggleWrap}>
89+
Wrap Text
90+
</PopoverItem>
91+
</>
92+
)}
93+
</PopoverContent>
94+
</Popover>,
95+
document.body
96+
)
97+
}

0 commit comments

Comments
 (0)