Skip to content

Commit d4c171c

Browse files
fix(sortOrder): initial ordering must be deterministic (#2833)
* fix(sortOrder): initial ordering must be deterministic * fix initial ordering issue * add created at to child item
1 parent 26d0799 commit d4c171c

File tree

5 files changed

+103
-23
lines changed

5 files changed

+103
-23
lines changed

apps/sim/app/api/workflows/route.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { workflow } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, eq, isNull, max } from 'drizzle-orm'
4+
import { and, asc, eq, isNull, min } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { getSession } from '@/lib/auth'
@@ -64,10 +64,20 @@ export async function GET(request: Request) {
6464

6565
let workflows
6666

67+
const orderByClause = [asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)]
68+
6769
if (workspaceId) {
68-
workflows = await db.select().from(workflow).where(eq(workflow.workspaceId, workspaceId))
70+
workflows = await db
71+
.select()
72+
.from(workflow)
73+
.where(eq(workflow.workspaceId, workspaceId))
74+
.orderBy(...orderByClause)
6975
} else {
70-
workflows = await db.select().from(workflow).where(eq(workflow.userId, userId))
76+
workflows = await db
77+
.select()
78+
.from(workflow)
79+
.where(eq(workflow.userId, userId))
80+
.orderBy(...orderByClause)
7181
}
7282

7383
return NextResponse.json({ data: workflows }, { status: 200 })
@@ -140,15 +150,15 @@ export async function POST(req: NextRequest) {
140150
sortOrder = providedSortOrder
141151
} else {
142152
const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId)
143-
const [maxResult] = await db
144-
.select({ maxOrder: max(workflow.sortOrder) })
153+
const [minResult] = await db
154+
.select({ minOrder: min(workflow.sortOrder) })
145155
.from(workflow)
146156
.where(
147157
workspaceId
148158
? and(eq(workflow.workspaceId, workspaceId), folderCondition)
149159
: and(eq(workflow.userId, session.user.id), folderCondition)
150160
)
151-
sortOrder = (maxResult?.maxOrder ?? -1) + 1
161+
sortOrder = (minResult?.minOrder ?? 1) - 1
152162
}
153163

154164
await db.insert(workflow).values({

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ const TREE_SPACING = {
1818
INDENT_PER_LEVEL: 20,
1919
} as const
2020

21+
function compareByOrder<T extends { sortOrder: number; createdAt?: Date; id: string }>(
22+
a: T,
23+
b: T
24+
): number {
25+
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
26+
const timeA = a.createdAt?.getTime() ?? 0
27+
const timeB = b.createdAt?.getTime() ?? 0
28+
if (timeA !== timeB) return timeA - timeB
29+
return a.id.localeCompare(b.id)
30+
}
31+
2132
interface WorkflowListProps {
2233
regularWorkflows: WorkflowMetadata[]
2334
isLoading?: boolean
@@ -97,7 +108,7 @@ export function WorkflowList({
97108
{} as Record<string, WorkflowMetadata[]>
98109
)
99110
for (const folderId of Object.keys(grouped)) {
100-
grouped[folderId].sort((a, b) => a.sortOrder - b.sortOrder)
111+
grouped[folderId].sort(compareByOrder)
101112
}
102113
return grouped
103114
}, [regularWorkflows])
@@ -208,13 +219,15 @@ export function WorkflowList({
208219
type: 'folder' | 'workflow'
209220
id: string
210221
sortOrder: number
222+
createdAt?: Date
211223
data: FolderTreeNode | WorkflowMetadata
212224
}> = []
213225
for (const childFolder of folder.children) {
214226
childItems.push({
215227
type: 'folder',
216228
id: childFolder.id,
217229
sortOrder: childFolder.sortOrder,
230+
createdAt: childFolder.createdAt,
218231
data: childFolder,
219232
})
220233
}
@@ -223,10 +236,11 @@ export function WorkflowList({
223236
type: 'workflow',
224237
id: workflow.id,
225238
sortOrder: workflow.sortOrder,
239+
createdAt: workflow.createdAt,
226240
data: workflow,
227241
})
228242
}
229-
childItems.sort((a, b) => a.sortOrder - b.sortOrder)
243+
childItems.sort(compareByOrder)
230244

231245
return (
232246
<div key={folder.id} className='relative'>
@@ -294,20 +308,28 @@ export function WorkflowList({
294308
type: 'folder' | 'workflow'
295309
id: string
296310
sortOrder: number
311+
createdAt?: Date
297312
data: FolderTreeNode | WorkflowMetadata
298313
}> = []
299314
for (const folder of folderTree) {
300-
items.push({ type: 'folder', id: folder.id, sortOrder: folder.sortOrder, data: folder })
315+
items.push({
316+
type: 'folder',
317+
id: folder.id,
318+
sortOrder: folder.sortOrder,
319+
createdAt: folder.createdAt,
320+
data: folder,
321+
})
301322
}
302323
for (const workflow of rootWorkflows) {
303324
items.push({
304325
type: 'workflow',
305326
id: workflow.id,
306327
sortOrder: workflow.sortOrder,
328+
createdAt: workflow.createdAt,
307329
data: workflow,
308330
})
309331
}
310-
return items.sort((a, b) => a.sortOrder - b.sortOrder)
332+
return items.sort(compareByOrder)
311333
}, [folderTree, rootWorkflows])
312334

313335
const hasRootItems = rootItems.length > 0

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,20 @@ export function useDragDrop() {
133133
[]
134134
)
135135

136-
type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number }
136+
type SiblingItem = {
137+
type: 'folder' | 'workflow'
138+
id: string
139+
sortOrder: number
140+
createdAt: Date
141+
}
142+
143+
const compareSiblingItems = (a: SiblingItem, b: SiblingItem): number => {
144+
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
145+
const timeA = a.createdAt.getTime()
146+
const timeB = b.createdAt.getTime()
147+
if (timeA !== timeB) return timeA - timeB
148+
return a.id.localeCompare(b.id)
149+
}
137150

138151
const getDestinationFolderId = useCallback((indicator: DropIndicator): string | null => {
139152
return indicator.position === 'inside'
@@ -202,11 +215,21 @@ export function useDragDrop() {
202215
return [
203216
...Object.values(currentFolders)
204217
.filter((f) => f.parentId === folderId)
205-
.map((f) => ({ type: 'folder' as const, id: f.id, sortOrder: f.sortOrder })),
218+
.map((f) => ({
219+
type: 'folder' as const,
220+
id: f.id,
221+
sortOrder: f.sortOrder,
222+
createdAt: f.createdAt,
223+
})),
206224
...Object.values(currentWorkflows)
207225
.filter((w) => w.folderId === folderId)
208-
.map((w) => ({ type: 'workflow' as const, id: w.id, sortOrder: w.sortOrder })),
209-
].sort((a, b) => a.sortOrder - b.sortOrder)
226+
.map((w) => ({
227+
type: 'workflow' as const,
228+
id: w.id,
229+
sortOrder: w.sortOrder,
230+
createdAt: w.createdAt,
231+
})),
232+
].sort(compareSiblingItems)
210233
}, [])
211234

212235
const setNormalizedDropIndicator = useCallback(
@@ -299,8 +322,9 @@ export function useDragDrop() {
299322
type: 'workflow' as const,
300323
id,
301324
sortOrder: currentWorkflows[id]?.sortOrder ?? 0,
325+
createdAt: currentWorkflows[id]?.createdAt ?? new Date(),
302326
}))
303-
.sort((a, b) => a.sortOrder - b.sortOrder)
327+
.sort(compareSiblingItems)
304328

305329
const insertAt = calculateInsertIndex(remaining, indicator)
306330

@@ -369,7 +393,12 @@ export function useDragDrop() {
369393

370394
const newOrder: SiblingItem[] = [
371395
...remaining.slice(0, insertAt),
372-
{ type: 'folder', id: draggedFolderId, sortOrder: 0 },
396+
{
397+
type: 'folder',
398+
id: draggedFolderId,
399+
sortOrder: 0,
400+
createdAt: draggedFolder?.createdAt ?? new Date(),
401+
},
373402
...remaining.slice(insertAt),
374403
]
375404

apps/sim/hooks/queries/workflows.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export function useCreateWorkflow() {
194194
const workflowsInFolder = Object.values(currentWorkflows).filter(
195195
(w) => w.folderId === targetFolderId
196196
)
197-
sortOrder = workflowsInFolder.reduce((max, w) => Math.max(max, w.sortOrder ?? 0), -1) + 1
197+
sortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1) - 1
198198
}
199199

200200
return {
@@ -294,7 +294,7 @@ export function useDuplicateWorkflowMutation() {
294294
const workflowsInFolder = Object.values(currentWorkflows).filter(
295295
(w) => w.folderId === targetFolderId
296296
)
297-
const maxSortOrder = workflowsInFolder.reduce((max, w) => Math.max(max, w.sortOrder ?? 0), -1)
297+
const minSortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1)
298298

299299
return {
300300
id: tempId,
@@ -305,7 +305,7 @@ export function useDuplicateWorkflowMutation() {
305305
color: variables.color,
306306
workspaceId: variables.workspaceId,
307307
folderId: targetFolderId,
308-
sortOrder: maxSortOrder + 1,
308+
sortOrder: minSortOrder - 1,
309309
}
310310
}
311311
)

apps/sim/lib/workflows/persistence/duplicate.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { eq } from 'drizzle-orm'
4+
import { and, eq, isNull, min } from 'drizzle-orm'
55
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
66
import type { Variable } from '@/stores/panel/variables/types'
77
import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types'
@@ -26,6 +26,7 @@ interface DuplicateWorkflowResult {
2626
color: string
2727
workspaceId: string
2828
folderId: string | null
29+
sortOrder: number
2930
blocksCount: number
3031
edgesCount: number
3132
subflowsCount: number
@@ -88,12 +89,29 @@ export async function duplicateWorkflow(
8889
throw new Error('Source workflow not found or access denied')
8990
}
9091

92+
const targetWorkspaceId = workspaceId || source.workspaceId
93+
const targetFolderId = folderId !== undefined ? folderId : source.folderId
94+
const folderCondition = targetFolderId
95+
? eq(workflow.folderId, targetFolderId)
96+
: isNull(workflow.folderId)
97+
98+
const [minResult] = await tx
99+
.select({ minOrder: min(workflow.sortOrder) })
100+
.from(workflow)
101+
.where(
102+
targetWorkspaceId
103+
? and(eq(workflow.workspaceId, targetWorkspaceId), folderCondition)
104+
: and(eq(workflow.userId, userId), folderCondition)
105+
)
106+
const sortOrder = (minResult?.minOrder ?? 1) - 1
107+
91108
// Create the new workflow first (required for foreign key constraints)
92109
await tx.insert(workflow).values({
93110
id: newWorkflowId,
94111
userId,
95-
workspaceId: workspaceId || source.workspaceId,
96-
folderId: folderId !== undefined ? folderId : source.folderId,
112+
workspaceId: targetWorkspaceId,
113+
folderId: targetFolderId,
114+
sortOrder,
97115
name,
98116
description: description || source.description,
99117
color: color || source.color,
@@ -286,7 +304,8 @@ export async function duplicateWorkflow(
286304
description: description || source.description,
287305
color: color || source.color,
288306
workspaceId: finalWorkspaceId,
289-
folderId: folderId !== undefined ? folderId : source.folderId,
307+
folderId: targetFolderId,
308+
sortOrder,
290309
blocksCount: sourceBlocks.length,
291310
edgesCount: sourceEdges.length,
292311
subflowsCount: sourceSubflows.length,

0 commit comments

Comments
 (0)