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
This commit is contained in:
Vikhyath Mondreti
2026-01-14 22:21:49 -08:00
committed by GitHub
parent 26d0799d22
commit d4c171c6d7
5 changed files with 103 additions and 23 deletions
+16 -6
View File
@@ -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'
@@ -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 })
@@ -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({
@@ -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
@@ -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])
@@ -208,6 +219,7 @@ export function WorkflowList({
type: 'folder' | 'workflow'
id: string
sortOrder: number
createdAt?: Date
data: FolderTreeNode | WorkflowMetadata
}> = []
for (const childFolder of folder.children) {
@@ -215,6 +227,7 @@ export function WorkflowList({
type: 'folder',
id: childFolder.id,
sortOrder: childFolder.sortOrder,
createdAt: childFolder.createdAt,
data: childFolder,
})
}
@@ -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'>
@@ -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
@@ -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'
@@ -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(
@@ -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)
@@ -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),
]
+3 -3
View File
@@ -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 {
@@ -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,
@@ -305,7 +305,7 @@ export function useDuplicateWorkflowMutation() {
color: variables.color,
workspaceId: variables.workspaceId,
folderId: targetFolderId,
sortOrder: maxSortOrder + 1,
sortOrder: minSortOrder - 1,
}
}
)
@@ -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'
@@ -26,6 +26,7 @@ interface DuplicateWorkflowResult {
color: string
workspaceId: string
folderId: string | null
sortOrder: number
blocksCount: number
edgesCount: number
subflowsCount: number
@@ -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,
@@ -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,