Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 16 additions & 6 deletions apps/sim/app/api/workflows/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull, max } from 'drizzle-orm'
import { and, asc, eq, isNull, min } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
Expand Down Expand Up @@ -64,10 +64,20 @@ export async function GET(request: Request) {

let workflows

const orderByClause = [asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)]

if (workspaceId) {
workflows = await db.select().from(workflow).where(eq(workflow.workspaceId, workspaceId))
workflows = await db
.select()
.from(workflow)
.where(eq(workflow.workspaceId, workspaceId))
.orderBy(...orderByClause)
} else {
workflows = await db.select().from(workflow).where(eq(workflow.userId, userId))
workflows = await db
.select()
.from(workflow)
.where(eq(workflow.userId, userId))
.orderBy(...orderByClause)
}

return NextResponse.json({ data: workflows }, { status: 200 })
Expand Down Expand Up @@ -140,15 +150,15 @@ export async function POST(req: NextRequest) {
sortOrder = providedSortOrder
} else {
const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId)
const [maxResult] = await db
.select({ maxOrder: max(workflow.sortOrder) })
const [minResult] = await db
.select({ minOrder: min(workflow.sortOrder) })
.from(workflow)
.where(
workspaceId
? and(eq(workflow.workspaceId, workspaceId), folderCondition)
: and(eq(workflow.userId, session.user.id), folderCondition)
)
sortOrder = (maxResult?.maxOrder ?? -1) + 1
sortOrder = (minResult?.minOrder ?? 1) - 1
}

await db.insert(workflow).values({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ const TREE_SPACING = {
INDENT_PER_LEVEL: 20,
} as const

function compareByOrder<T extends { sortOrder: number; createdAt?: Date; id: string }>(
a: T,
b: T
): number {
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
const timeA = a.createdAt?.getTime() ?? 0
const timeB = b.createdAt?.getTime() ?? 0
if (timeA !== timeB) return timeA - timeB
return a.id.localeCompare(b.id)
}

interface WorkflowListProps {
regularWorkflows: WorkflowMetadata[]
isLoading?: boolean
Expand Down Expand Up @@ -97,7 +108,7 @@ export function WorkflowList({
{} as Record<string, WorkflowMetadata[]>
)
for (const folderId of Object.keys(grouped)) {
grouped[folderId].sort((a, b) => a.sortOrder - b.sortOrder)
grouped[folderId].sort(compareByOrder)
}
return grouped
}, [regularWorkflows])
Expand Down Expand Up @@ -208,13 +219,15 @@ export function WorkflowList({
type: 'folder' | 'workflow'
id: string
sortOrder: number
createdAt?: Date
data: FolderTreeNode | WorkflowMetadata
}> = []
for (const childFolder of folder.children) {
childItems.push({
type: 'folder',
id: childFolder.id,
sortOrder: childFolder.sortOrder,
createdAt: childFolder.createdAt,
data: childFolder,
})
}
Expand All @@ -223,10 +236,11 @@ export function WorkflowList({
type: 'workflow',
id: workflow.id,
sortOrder: workflow.sortOrder,
createdAt: workflow.createdAt,
data: workflow,
})
}
childItems.sort((a, b) => a.sortOrder - b.sortOrder)
childItems.sort(compareByOrder)

return (
<div key={folder.id} className='relative'>
Expand Down Expand Up @@ -294,20 +308,28 @@ export function WorkflowList({
type: 'folder' | 'workflow'
id: string
sortOrder: number
createdAt?: Date
data: FolderTreeNode | WorkflowMetadata
}> = []
for (const folder of folderTree) {
items.push({ type: 'folder', id: folder.id, sortOrder: folder.sortOrder, data: folder })
items.push({
type: 'folder',
id: folder.id,
sortOrder: folder.sortOrder,
createdAt: folder.createdAt,
data: folder,
})
}
for (const workflow of rootWorkflows) {
items.push({
type: 'workflow',
id: workflow.id,
sortOrder: workflow.sortOrder,
createdAt: workflow.createdAt,
data: workflow,
})
}
return items.sort((a, b) => a.sortOrder - b.sortOrder)
return items.sort(compareByOrder)
}, [folderTree, rootWorkflows])

const hasRootItems = rootItems.length > 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,20 @@ export function useDragDrop() {
[]
)

type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number }
type SiblingItem = {
type: 'folder' | 'workflow'
id: string
sortOrder: number
createdAt: Date
}

const compareSiblingItems = (a: SiblingItem, b: SiblingItem): number => {
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
const timeA = a.createdAt.getTime()
const timeB = b.createdAt.getTime()
if (timeA !== timeB) return timeA - timeB
return a.id.localeCompare(b.id)
}

const getDestinationFolderId = useCallback((indicator: DropIndicator): string | null => {
return indicator.position === 'inside'
Expand Down Expand Up @@ -202,11 +215,21 @@ export function useDragDrop() {
return [
...Object.values(currentFolders)
.filter((f) => f.parentId === folderId)
.map((f) => ({ type: 'folder' as const, id: f.id, sortOrder: f.sortOrder })),
.map((f) => ({
type: 'folder' as const,
id: f.id,
sortOrder: f.sortOrder,
createdAt: f.createdAt,
})),
...Object.values(currentWorkflows)
.filter((w) => w.folderId === folderId)
.map((w) => ({ type: 'workflow' as const, id: w.id, sortOrder: w.sortOrder })),
].sort((a, b) => a.sortOrder - b.sortOrder)
.map((w) => ({
type: 'workflow' as const,
id: w.id,
sortOrder: w.sortOrder,
createdAt: w.createdAt,
})),
].sort(compareSiblingItems)
}, [])

const setNormalizedDropIndicator = useCallback(
Expand Down Expand Up @@ -299,8 +322,9 @@ export function useDragDrop() {
type: 'workflow' as const,
id,
sortOrder: currentWorkflows[id]?.sortOrder ?? 0,
createdAt: currentWorkflows[id]?.createdAt ?? new Date(),
}))
.sort((a, b) => a.sortOrder - b.sortOrder)
.sort(compareSiblingItems)

const insertAt = calculateInsertIndex(remaining, indicator)

Expand Down Expand Up @@ -369,7 +393,12 @@ export function useDragDrop() {

const newOrder: SiblingItem[] = [
...remaining.slice(0, insertAt),
{ type: 'folder', id: draggedFolderId, sortOrder: 0 },
{
type: 'folder',
id: draggedFolderId,
sortOrder: 0,
createdAt: draggedFolder?.createdAt ?? new Date(),
},
...remaining.slice(insertAt),
]

Expand Down
6 changes: 3 additions & 3 deletions apps/sim/hooks/queries/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export function useCreateWorkflow() {
const workflowsInFolder = Object.values(currentWorkflows).filter(
(w) => w.folderId === targetFolderId
)
sortOrder = workflowsInFolder.reduce((max, w) => Math.max(max, w.sortOrder ?? 0), -1) + 1
sortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1) - 1
}

return {
Expand Down Expand Up @@ -294,7 +294,7 @@ export function useDuplicateWorkflowMutation() {
const workflowsInFolder = Object.values(currentWorkflows).filter(
(w) => w.folderId === targetFolderId
)
const maxSortOrder = workflowsInFolder.reduce((max, w) => Math.max(max, w.sortOrder ?? 0), -1)
const minSortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1)

return {
id: tempId,
Expand All @@ -305,7 +305,7 @@ export function useDuplicateWorkflowMutation() {
color: variables.color,
workspaceId: variables.workspaceId,
folderId: targetFolderId,
sortOrder: maxSortOrder + 1,
sortOrder: minSortOrder - 1,
}
}
)
Expand Down
27 changes: 23 additions & 4 deletions apps/sim/lib/workflows/persistence/duplicate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { and, eq, isNull, min } from 'drizzle-orm'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import type { Variable } from '@/stores/panel/variables/types'
import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types'
Expand All @@ -26,6 +26,7 @@ interface DuplicateWorkflowResult {
color: string
workspaceId: string
folderId: string | null
sortOrder: number
blocksCount: number
edgesCount: number
subflowsCount: number
Expand Down Expand Up @@ -88,12 +89,29 @@ export async function duplicateWorkflow(
throw new Error('Source workflow not found or access denied')
}

const targetWorkspaceId = workspaceId || source.workspaceId
const targetFolderId = folderId !== undefined ? folderId : source.folderId
const folderCondition = targetFolderId
? eq(workflow.folderId, targetFolderId)
: isNull(workflow.folderId)

const [minResult] = await tx
.select({ minOrder: min(workflow.sortOrder) })
.from(workflow)
.where(
targetWorkspaceId
? and(eq(workflow.workspaceId, targetWorkspaceId), folderCondition)
: and(eq(workflow.userId, userId), folderCondition)
)
const sortOrder = (minResult?.minOrder ?? 1) - 1

// Create the new workflow first (required for foreign key constraints)
await tx.insert(workflow).values({
id: newWorkflowId,
userId,
workspaceId: workspaceId || source.workspaceId,
folderId: folderId !== undefined ? folderId : source.folderId,
workspaceId: targetWorkspaceId,
folderId: targetFolderId,
sortOrder,
name,
description: description || source.description,
color: color || source.color,
Expand Down Expand Up @@ -286,7 +304,8 @@ export async function duplicateWorkflow(
description: description || source.description,
color: color || source.color,
workspaceId: finalWorkspaceId,
folderId: folderId !== undefined ? folderId : source.folderId,
folderId: targetFolderId,
sortOrder,
blocksCount: sourceBlocks.length,
edgesCount: sourceEdges.length,
subflowsCount: sourceSubflows.length,
Expand Down