Skip to content

Commit 05bbf34

Browse files
waleedlatif1icecrasher321emir-karabeg
authored
improvement(canvas): add multi-block select, add batch handle, enabled, and edge operations (#2738)
* improvement(canvas): add multi-block select, add batch handle, enabled, and edge operations * feat(i18n): update translations (#2732) Co-authored-by: icecrasher321 <[email protected]> * don't allow flip handles for subflows * ack PR comments * more * fix missing handler * remove dead subflow-specific ops * remove unused code * fixed subflow ops * keep edges on subflow actions intact * fix subflow resizing * fix remove from subflow bulk * improvement(canvas): add multi-block select, add batch handle, enabled, and edge operations * don't allow flip handles for subflows * ack PR comments * more * fix missing handler * remove dead subflow-specific ops * remove unused code * fixed subflow ops * fix subflow resizing * keep edges on subflow actions intact * fixed copy from inside subflow * types improvement, preview fixes * fetch varible data in deploy modal * moved remove from subflow one position to the right * fix subflow issues * address greptile comment * fix test * improvement(preview): ui/ux * fix(preview): subflows * added batch add edges * removed recovery * use consolidated consts for sockets operations * more --------- Co-authored-by: icecrasher321 <[email protected]> Co-authored-by: Vikhyath Mondreti <[email protected]> Co-authored-by: Emir Karabeg <[email protected]>
1 parent 753600e commit 05bbf34

File tree

110 files changed

+5922
-2397
lines changed

Some content is hidden

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

110 files changed

+5922
-2397
lines changed

apps/sim/app/(landing)/privacy/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,7 @@ export default function PrivacyPolicy() {
767767
768768
</Link>
769769
</li>
770-
<li>Mailing Address: Sim, 80 Langton St, San Francisco, CA 94133, USA</li>
770+
<li>Mailing Address: Sim, 80 Langton St, San Francisco, CA 94103, USA</li>
771771
</ul>
772772
<p>We will respond to your request within a reasonable timeframe.</p>
773773
</section>

apps/sim/app/_styles/globals.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,40 @@
4242
animation: dash-animation 1.5s linear infinite !important;
4343
}
4444

45+
/**
46+
* React Flow selection box styling
47+
* Uses brand-secondary color for selection highlighting
48+
*/
49+
.react-flow__selection {
50+
background: rgba(51, 180, 255, 0.08) !important;
51+
border: 1px solid var(--brand-secondary) !important;
52+
}
53+
54+
.react-flow__nodesselection-rect,
55+
.react-flow__nodesselection {
56+
background: transparent !important;
57+
border: none !important;
58+
pointer-events: none !important;
59+
}
60+
61+
/**
62+
* Selected node ring indicator
63+
* Uses a pseudo-element overlay to match the original behavior (absolute inset-0 z-40)
64+
*/
65+
.react-flow__node.selected > div > div {
66+
position: relative;
67+
}
68+
69+
.react-flow__node.selected > div > div::after {
70+
content: "";
71+
position: absolute;
72+
inset: 0;
73+
z-index: 40;
74+
border-radius: 8px;
75+
box-shadow: 0 0 0 1.75px var(--brand-secondary);
76+
pointer-events: none;
77+
}
78+
4579
/**
4680
* Color tokens - single source of truth for all colors
4781
* Light mode: Warm theme

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ export async function POST(
253253
userId: deployment.userId,
254254
workspaceId,
255255
isDeployed: workflowRecord?.isDeployed ?? false,
256-
variables: workflowRecord?.variables || {},
256+
variables: (workflowRecord?.variables as Record<string, unknown>) ?? undefined,
257257
}
258258

259259
const stream = await createStreamingResponse({

apps/sim/app/api/templates/[id]/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
extractRequiredCredentials,
1111
sanitizeCredentials,
1212
} from '@/lib/workflows/credentials/credential-extractor'
13+
import type { WorkflowState } from '@/stores/workflows/workflow/types'
1314

1415
const logger = createLogger('TemplateByIdAPI')
1516

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

192-
const currentState = {
193+
const currentState: Partial<WorkflowState> = {
193194
blocks: normalizedData.blocks,
194195
edges: normalizedData.edges,
195196
loops: normalizedData.loops,
196197
parallels: normalizedData.parallels,
197-
variables: workflowRecord?.variables || undefined,
198+
variables: (workflowRecord?.variables as WorkflowState['variables']) ?? undefined,
198199
lastSaved: Date.now(),
199200
}
200201

apps/sim/app/api/templates/[id]/use/route.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { v4 as uuidv4 } from 'uuid'
77
import { getSession } from '@/lib/auth'
88
import { generateRequestId } from '@/lib/core/utils/request'
99
import { getBaseUrl } from '@/lib/core/utils/urls'
10-
import { regenerateWorkflowStateIds } from '@/lib/workflows/persistence/utils'
10+
import {
11+
type RegenerateStateInput,
12+
regenerateWorkflowStateIds,
13+
} from '@/lib/workflows/persistence/utils'
1114

1215
const logger = createLogger('TemplateUseAPI')
1316

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

111115
// Step 3: Save the workflow state using the existing state endpoint (like imports do)
112116
// Ensure variables in state are remapped for the new workflow as well

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
}

apps/sim/app/templates/[id]/template.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
332332
return (
333333
<WorkflowPreview
334334
workflowState={template.state}
335-
showSubBlocks={true}
336335
height='100%'
337336
width='100%'
338337
isPannable={true}

apps/sim/app/templates/components/template-card.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ function TemplateCardInner({
204204
{normalizedState && isInView ? (
205205
<WorkflowPreview
206206
workflowState={normalizedState}
207-
showSubBlocks={false}
208207
height={180}
209208
width='100%'
210209
isPannable={false}

0 commit comments

Comments
 (0)