Skip to content

Commit f0d2224

Browse files
committed
progress
1 parent 7dc4919 commit f0d2224

File tree

13 files changed

+190
-77
lines changed

13 files changed

+190
-77
lines changed

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

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export async function POST(request: NextRequest) {
5858
}
5959

6060
const body = await request.json()
61-
const { name, workspaceId, parentId, color } = body
61+
const { name, workspaceId, parentId, color, sortOrder: providedSortOrder } = body
6262

6363
if (!name || !workspaceId) {
6464
return NextResponse.json({ error: 'Name and workspace ID are required' }, { status: 400 })
@@ -81,25 +81,26 @@ export async function POST(request: NextRequest) {
8181
// Generate a new ID
8282
const id = crypto.randomUUID()
8383

84-
// Use transaction to ensure sortOrder consistency
8584
const newFolder = await db.transaction(async (tx) => {
86-
// Get the next sort order for the parent (or root level)
87-
// Consider all folders in the workspace, not just those created by current user
88-
const existingFolders = await tx
89-
.select({ sortOrder: workflowFolder.sortOrder })
90-
.from(workflowFolder)
91-
.where(
92-
and(
93-
eq(workflowFolder.workspaceId, workspaceId),
94-
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
85+
let sortOrder: number
86+
if (providedSortOrder !== undefined) {
87+
sortOrder = providedSortOrder
88+
} else {
89+
const existingFolders = await tx
90+
.select({ sortOrder: workflowFolder.sortOrder })
91+
.from(workflowFolder)
92+
.where(
93+
and(
94+
eq(workflowFolder.workspaceId, workspaceId),
95+
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
96+
)
9597
)
96-
)
97-
.orderBy(desc(workflowFolder.sortOrder))
98-
.limit(1)
98+
.orderBy(desc(workflowFolder.sortOrder))
99+
.limit(1)
99100

100-
const nextSortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0
101+
sortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0
102+
}
101103

102-
// Insert the new folder within the same transaction
103104
const [folder] = await tx
104105
.insert(workflowFolder)
105106
.values({
@@ -109,7 +110,7 @@ export async function POST(request: NextRequest) {
109110
workspaceId,
110111
parentId: parentId || null,
111112
color: color || '#6B7280',
112-
sortOrder: nextSortOrder,
113+
sortOrder,
113114
})
114115
.returning()
115116

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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const CreateWorkflowSchema = z.object({
1717
color: z.string().optional().default('#3972F6'),
1818
workspaceId: z.string().optional(),
1919
folderId: z.string().nullable().optional(),
20+
sortOrder: z.number().int().optional(),
2021
})
2122

2223
// GET /api/workflows - Get workflows for user (optionally filtered by workspaceId)
@@ -93,7 +94,14 @@ export async function POST(req: NextRequest) {
9394

9495
try {
9596
const body = await req.json()
96-
const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body)
97+
const {
98+
name,
99+
description,
100+
color,
101+
workspaceId,
102+
folderId,
103+
sortOrder: providedSortOrder,
104+
} = CreateWorkflowSchema.parse(body)
97105

98106
if (workspaceId) {
99107
const workspacePermission = await getUserEntityPermissions(
@@ -131,16 +139,21 @@ export async function POST(req: NextRequest) {
131139
// Silently fail
132140
})
133141

134-
const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId)
135-
const [maxResult] = await db
136-
.select({ maxOrder: max(workflow.sortOrder) })
137-
.from(workflow)
138-
.where(
139-
workspaceId
140-
? and(eq(workflow.workspaceId, workspaceId), folderCondition)
141-
: and(eq(workflow.userId, session.user.id), folderCondition)
142-
)
143-
const sortOrder = (maxResult?.maxOrder ?? -1) + 1
142+
let sortOrder: number
143+
if (providedSortOrder !== undefined) {
144+
sortOrder = providedSortOrder
145+
} else {
146+
const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId)
147+
const [maxResult] = await db
148+
.select({ maxOrder: max(workflow.sortOrder) })
149+
.from(workflow)
150+
.where(
151+
workspaceId
152+
? and(eq(workflow.workspaceId, workspaceId), folderCondition)
153+
: and(eq(workflow.userId, session.user.id), folderCondition)
154+
)
155+
sortOrder = (maxResult?.maxOrder ?? -1) + 1
156+
}
144157

145158
await db.insert(workflow).values({
146159
id: workflowId,

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

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -113,36 +113,7 @@ export function WorkflowItem({
113113
[workflow.id, updateWorkflow]
114114
)
115115

116-
const onDragStart = useCallback(
117-
(e: React.DragEvent) => {
118-
if (isEditing) {
119-
e.preventDefault()
120-
return
121-
}
122-
123-
const workflowIds =
124-
isSelected && selectedWorkflows.size > 1 ? Array.from(selectedWorkflows) : [workflow.id]
125-
126-
e.dataTransfer.setData('workflow-ids', JSON.stringify(workflowIds))
127-
e.dataTransfer.effectAllowed = 'move'
128-
onDragStartProp?.()
129-
},
130-
[isSelected, selectedWorkflows, workflow.id, onDragStartProp]
131-
)
132-
133-
const {
134-
isDragging,
135-
shouldPreventClickRef,
136-
handleDragStart,
137-
handleDragEnd: handleDragEndBase,
138-
} = useItemDrag({
139-
onDragStart,
140-
})
141-
142-
const handleDragEnd = useCallback(() => {
143-
handleDragEndBase()
144-
onDragEndProp?.()
145-
}, [handleDragEndBase, onDragEndProp])
116+
const isEditingRef = useRef(false)
146117

147118
const {
148119
isOpen: isContextMenuOpen,
@@ -247,6 +218,43 @@ export function WorkflowItem({
247218
itemId: workflow.id,
248219
})
249220

221+
isEditingRef.current = isEditing
222+
223+
const onDragStart = useCallback(
224+
(e: React.DragEvent) => {
225+
if (isEditingRef.current) {
226+
e.preventDefault()
227+
return
228+
}
229+
230+
const currentSelection = useFolderStore.getState().selectedWorkflows
231+
const isCurrentlySelected = currentSelection.has(workflow.id)
232+
const workflowIds =
233+
isCurrentlySelected && currentSelection.size > 1
234+
? Array.from(currentSelection)
235+
: [workflow.id]
236+
237+
e.dataTransfer.setData('workflow-ids', JSON.stringify(workflowIds))
238+
e.dataTransfer.effectAllowed = 'move'
239+
onDragStartProp?.()
240+
},
241+
[workflow.id, onDragStartProp]
242+
)
243+
244+
const {
245+
isDragging,
246+
shouldPreventClickRef,
247+
handleDragStart,
248+
handleDragEnd: handleDragEndBase,
249+
} = useItemDrag({
250+
onDragStart,
251+
})
252+
253+
const handleDragEnd = useCallback(() => {
254+
handleDragEndBase()
255+
onDragEndProp?.()
256+
}, [handleDragEndBase, onDragEndProp])
257+
250258
/**
251259
* Handle double-click on workflow name to enter rename mode
252260
*/

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export function WorkflowList({
241241
</div>
242242
<DropIndicatorLine show={showAfter} level={level} />
243243

244-
{isExpanded && (
244+
{isExpanded && (hasChildren || isDragging) && (
245245
<div className='relative'>
246246
<div
247247
className='pointer-events-none absolute top-0 bottom-0 w-px bg-[var(--border)]'
@@ -253,7 +253,7 @@ export function WorkflowList({
253253
? renderFolderSection(item.data as FolderTreeNode, level + 1, folder.id)
254254
: renderWorkflowItem(item.data as WorkflowMetadata, level + 1, folder.id)
255255
)}
256-
{!hasChildren && (
256+
{!hasChildren && isDragging && (
257257
<div className='h-[24px]' {...createEmptyFolderDropZone(folder.id)} />
258258
)}
259259
</div>

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ export function useDragDrop() {
158158
const remaining = siblingItems.filter(
159159
(item) => !(item.type === 'workflow' && movingSet.has(item.id))
160160
)
161-
const moving = workflowIds.map((id) => ({ type: 'workflow' as const, id, sortOrder: 0 }))
161+
const moving = workflowIds
162+
.map((id) => {
163+
const w = currentWorkflows[id]
164+
return { type: 'workflow' as const, id, sortOrder: w?.sortOrder ?? 0 }
165+
})
166+
.sort((a, b) => a.sortOrder - b.sortOrder)
162167

163168
let insertAt: number
164169
if (indicator.position === 'inside') {

apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-folder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export function useExportFolder({ workspaceId, folderId, onSuccess }: UseExportF
165165
name: workflow.name,
166166
description: workflow.description,
167167
color: workflow.color,
168+
sortOrder: workflow.sortOrder,
168169
exportedAt: new Date().toISOString(),
169170
},
170171
variables: workflowVariables,

apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export function useExportWorkflow({ workspaceId, onSuccess }: UseExportWorkflowP
113113
name: workflow.name,
114114
description: workflow.description,
115115
color: workflow.color,
116+
sortOrder: workflow.sortOrder,
116117
exportedAt: new Date().toISOString(),
117118
},
118119
variables: workflowVariables,

apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workspace.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export function useExportWorkspace({ onSuccess }: UseExportWorkspaceProps = {})
8686
description: workflow.description,
8787
color: workflow.color,
8888
folderId: workflow.folderId,
89+
sortOrder: workflow.sortOrder,
8990
},
9091
state: workflowData.state,
9192
variables: workflowVariables,
@@ -100,6 +101,7 @@ export function useExportWorkspace({ onSuccess }: UseExportWorkspaceProps = {})
100101
id: folder.id,
101102
name: folder.name,
102103
parentId: folder.parentId,
104+
sortOrder: folder.sortOrder,
103105
})
104106
)
105107

apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
4040
* Import a single workflow
4141
*/
4242
const importSingleWorkflow = useCallback(
43-
async (content: string, filename: string, folderId?: string) => {
43+
async (content: string, filename: string, folderId?: string, sortOrder?: number) => {
4444
const { data: workflowData, errors: parseErrors } = parseWorkflowJson(content)
4545

4646
if (!workflowData || parseErrors.length > 0) {
@@ -60,6 +60,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
6060
description: workflowData.metadata?.description || 'Imported from JSON',
6161
workspaceId,
6262
folderId: folderId || undefined,
63+
sortOrder,
6364
})
6465
const newWorkflowId = result.id
6566

@@ -140,6 +141,36 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
140141
})
141142
const folderMap = new Map<string, string>()
142143

144+
const exportedFoldersByPath = new Map<string, { sortOrder?: number }>()
145+
if (metadata?.folders) {
146+
type ExportedFolder = {
147+
id: string
148+
name: string
149+
parentId: string | null
150+
sortOrder?: number
151+
}
152+
const foldersById = new Map<string, ExportedFolder>(
153+
metadata.folders.map((f) => [f.id, f])
154+
)
155+
const sanitizeName = (name: string) => name.replace(/[^a-z0-9-_]/gi, '-')
156+
157+
const buildPath = (folderId: string): string => {
158+
const pathParts: string[] = []
159+
let currentId: string | null = folderId
160+
while (currentId && foldersById.has(currentId)) {
161+
const folder: ExportedFolder = foldersById.get(currentId)!
162+
pathParts.unshift(sanitizeName(folder.name))
163+
currentId = folder.parentId
164+
}
165+
return pathParts.join('/')
166+
}
167+
168+
for (const f of metadata.folders) {
169+
const path = buildPath(f.id)
170+
exportedFoldersByPath.set(path, { sortOrder: f.sortOrder })
171+
}
172+
}
173+
143174
for (const workflow of extractedWorkflows) {
144175
try {
145176
let targetFolderId = importFolder.id
@@ -152,12 +183,15 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
152183

153184
for (let i = 0; i < workflow.folderPath.length; i++) {
154185
const pathSegment = workflow.folderPath.slice(0, i + 1).join('/')
186+
const folderNameForSegment = workflow.folderPath[i]
155187

156188
if (!folderMap.has(pathSegment)) {
189+
const exportedFolder = exportedFoldersByPath.get(pathSegment)
157190
const subFolder = await createFolderMutation.mutateAsync({
158-
name: workflow.folderPath[i],
191+
name: folderNameForSegment,
159192
workspaceId,
160193
parentId,
194+
sortOrder: exportedFolder?.sortOrder,
161195
})
162196
folderMap.set(pathSegment, subFolder.id)
163197
parentId = subFolder.id
@@ -173,7 +207,8 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
173207
const workflowId = await importSingleWorkflow(
174208
workflow.content,
175209
workflow.name,
176-
targetFolderId
210+
targetFolderId,
211+
workflow.sortOrder
177212
)
178213
if (workflowId) importedWorkflowIds.push(workflowId)
179214
} catch (error) {

apps/sim/hooks/queries/folders.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ interface CreateFolderVariables {
6868
name: string
6969
parentId?: string
7070
color?: string
71+
sortOrder?: number
7172
}
7273

7374
interface UpdateFolderVariables {
@@ -160,18 +161,20 @@ export function useCreateFolder() {
160161
parentId: variables.parentId || null,
161162
color: variables.color || '#808080',
162163
isExpanded: false,
163-
sortOrder: getNextSortOrder(previousFolders, variables.workspaceId, variables.parentId),
164+
sortOrder:
165+
variables.sortOrder ??
166+
getNextSortOrder(previousFolders, variables.workspaceId, variables.parentId),
164167
createdAt: new Date(),
165168
updatedAt: new Date(),
166169
})
167170
)
168171

169172
return useMutation({
170-
mutationFn: async ({ workspaceId, ...payload }: CreateFolderVariables) => {
173+
mutationFn: async ({ workspaceId, sortOrder, ...payload }: CreateFolderVariables) => {
171174
const response = await fetch('/api/folders', {
172175
method: 'POST',
173176
headers: { 'Content-Type': 'application/json' },
174-
body: JSON.stringify({ ...payload, workspaceId }),
177+
body: JSON.stringify({ ...payload, workspaceId, sortOrder }),
175178
})
176179

177180
if (!response.ok) {

0 commit comments

Comments
 (0)