mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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({
|
||||
|
||||
+26
-4
@@ -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),
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user