Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1a5dda6
improvement(canvas): add multi-block select, add batch handle, enable…
waleedlatif1 Jan 9, 2026
dc0040d
feat(i18n): update translations (#2732)
waleedlatif1 Jan 9, 2026
e054cef
don't allow flip handles for subflows
waleedlatif1 Jan 9, 2026
757343d
ack PR comments
waleedlatif1 Jan 9, 2026
e1036e3
more
waleedlatif1 Jan 9, 2026
a8fb76b
fix missing handler
waleedlatif1 Jan 9, 2026
0568704
remove dead subflow-specific ops
waleedlatif1 Jan 9, 2026
1029ba0
remove unused code
waleedlatif1 Jan 9, 2026
20d66b9
fixed subflow ops
waleedlatif1 Jan 9, 2026
6e7f3da
keep edges on subflow actions intact
waleedlatif1 Jan 9, 2026
4fa6cb8
fix subflow resizing
icecrasher321 Jan 9, 2026
ee7a561
Merge branch 'fix/select' of github.com:simstudioai/sim into fix/select
icecrasher321 Jan 9, 2026
b41d17c
fix remove from subflow bulk
icecrasher321 Jan 9, 2026
0f515c3
improvement(canvas): add multi-block select, add batch handle, enable…
waleedlatif1 Jan 9, 2026
aca9579
don't allow flip handles for subflows
waleedlatif1 Jan 9, 2026
b11f1cb
ack PR comments
waleedlatif1 Jan 9, 2026
06c007f
more
waleedlatif1 Jan 9, 2026
abf64aa
fix missing handler
waleedlatif1 Jan 9, 2026
8af27a7
remove dead subflow-specific ops
waleedlatif1 Jan 9, 2026
3b69707
remove unused code
waleedlatif1 Jan 9, 2026
abf46da
fixed subflow ops
waleedlatif1 Jan 9, 2026
24b918a
fix subflow resizing
icecrasher321 Jan 9, 2026
60e25fd
keep edges on subflow actions intact
waleedlatif1 Jan 9, 2026
3f37b5c
fixed copy from inside subflow
waleedlatif1 Jan 9, 2026
ea8c710
Merge branch 'fix/select' of github.com:simstudioai/sim into fix/select
icecrasher321 Jan 9, 2026
7022b4c
types improvement, preview fixes
waleedlatif1 Jan 9, 2026
c97bc69
fetch varible data in deploy modal
waleedlatif1 Jan 9, 2026
b085942
moved remove from subflow one position to the right
waleedlatif1 Jan 9, 2026
98493de
fix subflow issues
icecrasher321 Jan 9, 2026
e7705d5
address greptile comment
icecrasher321 Jan 9, 2026
7f312cb
fix test
icecrasher321 Jan 9, 2026
37c13c8
improvement(preview): ui/ux
emir-karabeg Jan 9, 2026
f00a7a0
fix(preview): subflows
emir-karabeg Jan 9, 2026
b24f119
added batch add edges
waleedlatif1 Jan 9, 2026
687733d
removed recovery
waleedlatif1 Jan 9, 2026
80e98dd
use consolidated consts for sockets operations
waleedlatif1 Jan 9, 2026
9026952
more
waleedlatif1 Jan 9, 2026
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
2 changes: 1 addition & 1 deletion apps/sim/app/(landing)/privacy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ export default function PrivacyPolicy() {
privacy@sim.ai
</Link>
</li>
<li>Mailing Address: Sim, 80 Langton St, San Francisco, CA 94133, USA</li>
<li>Mailing Address: Sim, 80 Langton St, San Francisco, CA 94103, USA</li>
</ul>
<p>We will respond to your request within a reasonable timeframe.</p>
</section>
Expand Down
34 changes: 34 additions & 0 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@
animation: dash-animation 1.5s linear infinite !important;
}

/**
* React Flow selection box styling
* Uses brand-secondary color for selection highlighting
*/
.react-flow__selection {
background: rgba(51, 180, 255, 0.08) !important;
border: 1px solid var(--brand-secondary) !important;
}

.react-flow__nodesselection-rect,
.react-flow__nodesselection {
background: transparent !important;
border: none !important;
pointer-events: none !important;
}

/**
* Selected node ring indicator
* Uses a pseudo-element overlay to match the original behavior (absolute inset-0 z-40)
*/
.react-flow__node.selected > div > div {
position: relative;
}

.react-flow__node.selected > div > div::after {
content: "";
position: absolute;
inset: 0;
z-index: 40;
border-radius: 8px;
box-shadow: 0 0 0 1.75px var(--brand-secondary);
pointer-events: none;
}

/**
* Color tokens - single source of truth for all colors
* Light mode: Warm theme
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/chat/[identifier]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export async function POST(
userId: deployment.userId,
workspaceId,
isDeployed: workflowRecord?.isDeployed ?? false,
variables: workflowRecord?.variables || {},
variables: (workflowRecord?.variables as Record<string, unknown>) ?? undefined,
}

const stream = await createStreamingResponse({
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/templates/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
extractRequiredCredentials,
sanitizeCredentials,
} from '@/lib/workflows/credentials/credential-extractor'
import type { WorkflowState } from '@/stores/workflows/workflow/types'

const logger = createLogger('TemplateByIdAPI')

Expand Down Expand Up @@ -189,12 +190,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
.where(eq(workflow.id, template.workflowId))
.limit(1)

const currentState = {
const currentState: Partial<WorkflowState> = {
blocks: normalizedData.blocks,
edges: normalizedData.edges,
loops: normalizedData.loops,
parallels: normalizedData.parallels,
variables: workflowRecord?.variables || undefined,
variables: (workflowRecord?.variables as WorkflowState['variables']) ?? undefined,
lastSaved: Date.now(),
}

Expand Down
10 changes: 7 additions & 3 deletions apps/sim/app/api/templates/[id]/use/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { v4 as uuidv4 } from 'uuid'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { regenerateWorkflowStateIds } from '@/lib/workflows/persistence/utils'
import {
type RegenerateStateInput,
regenerateWorkflowStateIds,
} from '@/lib/workflows/persistence/utils'

const logger = createLogger('TemplateUseAPI')

Expand Down Expand Up @@ -104,9 +107,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
// Step 2: Regenerate IDs when creating a copy (not when connecting/editing template)
// When connecting to template (edit mode), keep original IDs
// When using template (copy mode), regenerate all IDs to avoid conflicts
const templateState = templateData.state as RegenerateStateInput
const workflowState = connectToTemplate
? templateData.state
: regenerateWorkflowStateIds(templateData.state)
? templateState
: regenerateWorkflowStateIds(templateState)

// Step 3: Save the workflow state using the existing state endpoint (like imports do)
// Ensure variables in state are remapped for the new workflow as well
Expand Down
34 changes: 21 additions & 13 deletions apps/sim/app/api/v1/admin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export interface WorkflowExportState {
color?: string
exportedAt?: string
}
variables?: WorkflowVariable[]
variables?: Record<string, WorkflowVariable>
}

export interface WorkflowExportPayload {
Expand Down Expand Up @@ -317,36 +317,44 @@ export interface WorkspaceImportResponse {
// =============================================================================

/**
* Parse workflow variables from database JSON format to array format.
* Handles both array and Record<string, Variable> formats.
* Parse workflow variables from database JSON format to Record format.
* Handles both legacy Array and current Record<string, Variable> formats.
*/
export function parseWorkflowVariables(
dbVariables: DbWorkflow['variables']
): WorkflowVariable[] | undefined {
): Record<string, WorkflowVariable> | undefined {
if (!dbVariables) return undefined

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

// Handle legacy Array format by converting to Record
if (Array.isArray(varsObj)) {
return varsObj.map((v) => ({
id: v.id,
name: v.name,
type: v.type,
value: v.value,
}))
const result: Record<string, WorkflowVariable> = {}
for (const v of varsObj) {
result[v.id] = {
id: v.id,
name: v.name,
type: v.type,
value: v.value,
}
}
return result
}

// Already Record format - normalize and return
if (typeof varsObj === 'object' && varsObj !== null) {
return Object.values(varsObj).map((v: unknown) => {
const result: Record<string, WorkflowVariable> = {}
for (const [key, v] of Object.entries(varsObj)) {
const variable = v as { id: string; name: string; type: VariableType; value: unknown }
return {
result[key] = {
id: variable.id,
name: variable.name,
type: variable.type,
value: variable.value,
}
})
}
return result
}
} catch {
// pass
Expand Down
25 changes: 18 additions & 7 deletions apps/sim/app/api/workflows/[id]/variables/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,15 @@ describe('Workflow Variables API Route', () => {
update: { results: [{}] },
})

const variables = [
{ id: 'var-1', workflowId: 'workflow-123', name: 'test', type: 'string', value: 'hello' },
]
const variables = {
'var-1': {
id: 'var-1',
workflowId: 'workflow-123',
name: 'test',
type: 'string',
value: 'hello',
},
}

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

const variables = [
{ id: 'var-1', workflowId: 'workflow-123', name: 'test', type: 'string', value: 'hello' },
]
const variables = {
'var-1': {
id: 'var-1',
workflowId: 'workflow-123',
name: 'test',
type: 'string',
value: 'hello',
},
}

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

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

const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables', {
Expand Down
42 changes: 20 additions & 22 deletions apps/sim/app/api/workflows/[id]/variables/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ import type { Variable } from '@/stores/panel/variables/types'

const logger = createLogger('WorkflowVariablesAPI')

const VariableSchema = z.object({
id: z.string(),
workflowId: z.string(),
name: z.string(),
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'plain']),
value: z.union([
z.string(),
z.number(),
z.boolean(),
z.record(z.unknown()),
z.array(z.unknown()),
]),
})

const VariablesSchema = z.object({
variables: z.array(
z.object({
id: z.string(),
workflowId: z.string(),
name: z.string(),
type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'plain']),
value: z.union([z.string(), z.number(), z.boolean(), z.record(z.any()), z.array(z.any())]),
})
),
variables: z.record(z.string(), VariableSchema),
})

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

// Format variables for storage
const variablesRecord: Record<string, Variable> = {}
variables.forEach((variable) => {
variablesRecord[variable.id] = variable
})

// Replace variables completely with the incoming ones
// Variables are already in Record format - use directly
// The frontend is the source of truth for what variables should exist
const updatedVariables = variablesRecord

// Update workflow with variables
await db
.update(workflow)
.set({
variables: updatedVariables,
variables,
updatedAt: new Date(),
})
.where(eq(workflow.id, workflowId))
Expand Down Expand Up @@ -148,8 +145,9 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
headers,
}
)
} catch (error: any) {
} catch (error) {
logger.error(`[${requestId}] Workflow variables fetch error`, error)
return NextResponse.json({ error: error.message }, { status: 500 })
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
}
1 change: 0 additions & 1 deletion apps/sim/app/templates/[id]/template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
return (
<WorkflowPreview
workflowState={template.state}
showSubBlocks={true}
height='100%'
width='100%'
isPannable={true}
Expand Down
1 change: 0 additions & 1 deletion apps/sim/app/templates/components/template-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@ function TemplateCardInner({
{normalizedState && isInView ? (
<WorkflowPreview
workflowState={normalizedState}
showSubBlocks={false}
height={180}
width='100%'
isPannable={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SnapshotContextMenu } from './snapshot-context-menu'
Loading