fix edge cases

This commit is contained in:
Vikhyath Mondreti
2026-01-14 12:06:01 -08:00
parent f0d22246a7
commit 78d6082235
5 changed files with 359 additions and 212 deletions

View File

@@ -13,6 +13,7 @@ const logger = createLogger('Workspaces')
const createWorkspaceSchema = z.object({
name: z.string().trim().min(1, 'Name is required'),
skipDefaultWorkflow: z.boolean().optional().default(false),
})
// Get all workspaces for the current user
@@ -63,9 +64,9 @@ export async function POST(req: Request) {
}
try {
const { name } = createWorkspaceSchema.parse(await req.json())
const { name, skipDefaultWorkflow } = createWorkspaceSchema.parse(await req.json())
const newWorkspace = await createWorkspace(session.user.id, name)
const newWorkspace = await createWorkspace(session.user.id, name, skipDefaultWorkflow)
return NextResponse.json({ workspace: newWorkspace })
} catch (error) {
@@ -80,7 +81,7 @@ async function createDefaultWorkspace(userId: string, userName?: string | null)
return createWorkspace(userId, workspaceName)
}
async function createWorkspace(userId: string, name: string) {
async function createWorkspace(userId: string, name: string, skipDefaultWorkflow = false) {
const workspaceId = crypto.randomUUID()
const workflowId = crypto.randomUUID()
const now = new Date()
@@ -97,7 +98,6 @@ async function createWorkspace(userId: string, name: string) {
updatedAt: now,
})
// Create admin permissions for the workspace owner
await tx.insert(permissions).values({
id: crypto.randomUUID(),
entityType: 'workspace' as const,
@@ -108,37 +108,41 @@ async function createWorkspace(userId: string, name: string) {
updatedAt: now,
})
// Create initial workflow for the workspace (empty canvas)
// Create the workflow
await tx.insert(workflow).values({
id: workflowId,
userId,
workspaceId,
folderId: null,
name: 'default-agent',
description: 'Your first workflow - start building here!',
color: '#3972F6',
lastSynced: now,
createdAt: now,
updatedAt: now,
isDeployed: false,
runCount: 0,
variables: {},
})
if (!skipDefaultWorkflow) {
await tx.insert(workflow).values({
id: workflowId,
userId,
workspaceId,
folderId: null,
name: 'default-agent',
description: 'Your first workflow - start building here!',
color: '#3972F6',
lastSynced: now,
createdAt: now,
updatedAt: now,
isDeployed: false,
runCount: 0,
variables: {},
})
}
logger.info(
`Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`
skipDefaultWorkflow
? `Created workspace ${workspaceId} for user ${userId}`
: `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`
)
})
const { workflowState } = buildDefaultWorkflowArtifacts()
const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)
if (!skipDefaultWorkflow) {
const { workflowState } = buildDefaultWorkflowArtifacts()
const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)
if (!seedResult.success) {
throw new Error(seedResult.error || 'Failed to seed default workflow state')
if (!seedResult.success) {
throw new Error(seedResult.error || 'Failed to seed default workflow state')
}
}
} catch (error) {
logger.error(`Failed to create workspace ${workspaceId} with initial workflow:`, error)
logger.error(`Failed to create workspace ${workspaceId}:`, error)
throw error
}

View File

@@ -33,7 +33,7 @@ function DropIndicatorLine({ show, level = 0 }: { show: boolean; level?: number
className='pointer-events-none absolute left-0 right-0 z-20 flex items-center'
style={{ paddingLeft: `${level * TREE_SPACING.INDENT_PER_LEVEL}px` }}
>
<div className='h-[2px] flex-1 rounded-full bg-[#0096FF]' />
<div className='h-[2px] flex-1 rounded-full bg-gray-400/60' />
</div>
)
}
@@ -163,7 +163,7 @@ export function WorkflowList({
active={isWorkflowActive(workflow.id)}
level={level}
onWorkflowClick={handleWorkflowClick}
onDragStart={() => handleDragStart('workflow')}
onDragStart={() => handleDragStart('workflow', folderId)}
onDragEnd={handleDragEnd}
/>
</div>
@@ -224,18 +224,21 @@ export function WorkflowList({
return (
<div key={folder.id} className='relative'>
<DropIndicatorLine show={showBefore} level={level} />
{/* Drop target highlight overlay - covers entire folder section */}
<div
className={clsx(
'rounded-[4px] transition-colors duration-75',
showInside && isDragging && 'bg-[#0096FF]/10'
'pointer-events-none absolute inset-0 z-10 rounded-[4px] transition-opacity duration-75',
showInside && isDragging ? 'bg-gray-400/20 opacity-100' : 'opacity-0'
)}
/>
<div
style={{ paddingLeft: `${level * TREE_SPACING.INDENT_PER_LEVEL}px` }}
{...createFolderDragHandlers(folder.id, parentFolderId)}
>
<FolderItem
folder={folder}
level={level}
onDragStart={() => handleDragStart('folder')}
onDragStart={() => handleDragStart('folder', parentFolderId)}
onDragEnd={handleDragEnd}
/>
</div>
@@ -314,13 +317,16 @@ export function WorkflowList({
return (
<div className='flex min-h-full flex-col pb-[8px]' onClick={handleContainerClick}>
<div
className={clsx(
'relative flex-1 rounded-[4px] transition-colors duration-75',
!hasRootItems && 'min-h-[26px]',
showRootInside && isDragging && 'bg-[#0096FF]/10'
)}
className={clsx('relative flex-1 rounded-[4px]', !hasRootItems && 'min-h-[26px]')}
{...rootDropZoneHandlers}
>
{/* Root drop target highlight overlay */}
<div
className={clsx(
'pointer-events-none absolute inset-0 z-10 rounded-[4px] transition-opacity duration-75',
showRootInside && isDragging ? 'bg-gray-400/20 opacity-100' : 'opacity-0'
)}
/>
<div className='space-y-[2px]'>
{rootItems.map((item) =>
item.type === 'folder'

View File

@@ -27,6 +27,7 @@ export function useDragDrop() {
const hoverExpandTimerRef = useRef<number | null>(null)
const lastDragYRef = useRef<number>(0)
const draggedTypeRef = useRef<'workflow' | 'folder' | null>(null)
const draggedSourceFolderRef = useRef<string | null>(null)
const params = useParams()
const workspaceId = params.workspaceId as string | undefined
@@ -119,59 +120,163 @@ export function useDragDrop() {
[]
)
type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number }
const getDestinationFolderId = useCallback((indicator: DropIndicator): string | null => {
return indicator.position === 'inside'
? indicator.targetId === 'root'
? null
: indicator.targetId
: indicator.folderId
}, [])
const calculateInsertIndex = useCallback(
(remaining: SiblingItem[], indicator: DropIndicator): number => {
return indicator.position === 'inside'
? remaining.length
: remaining.findIndex((item) => item.id === indicator.targetId) +
(indicator.position === 'after' ? 1 : 0)
},
[]
)
const buildAndSubmitUpdates = useCallback(
async (newOrder: SiblingItem[], destinationFolderId: string | null) => {
const indexed = newOrder.map((item, i) => ({ ...item, sortOrder: i }))
const folderUpdates = indexed
.filter((item) => item.type === 'folder')
.map((item) => ({ id: item.id, sortOrder: item.sortOrder, parentId: destinationFolderId }))
const workflowUpdates = indexed
.filter((item) => item.type === 'workflow')
.map((item) => ({ id: item.id, sortOrder: item.sortOrder, folderId: destinationFolderId }))
await Promise.all([
folderUpdates.length > 0 &&
reorderFoldersMutation.mutateAsync({ workspaceId: workspaceId!, updates: folderUpdates }),
workflowUpdates.length > 0 &&
reorderWorkflowsMutation.mutateAsync({
workspaceId: workspaceId!,
updates: workflowUpdates,
}),
])
},
[workspaceId, reorderFoldersMutation, reorderWorkflowsMutation]
)
const isLeavingElement = useCallback((e: React.DragEvent<HTMLElement>): boolean => {
const relatedTarget = e.relatedTarget as HTMLElement | null
const currentTarget = e.currentTarget as HTMLElement
return !relatedTarget || !currentTarget.contains(relatedTarget)
}, [])
const initDragOver = useCallback((e: React.DragEvent<HTMLElement>, stopPropagation = true) => {
e.preventDefault()
if (stopPropagation) e.stopPropagation()
lastDragYRef.current = e.clientY
setIsDragging(true)
}, [])
const getSiblingItems = useCallback((folderId: string | null): SiblingItem[] => {
const currentFolders = useFolderStore.getState().folders
const currentWorkflows = useWorkflowRegistry.getState().workflows
return [
...Object.values(currentFolders)
.filter((f) => f.parentId === folderId)
.map((f) => ({ type: 'folder' as const, id: f.id, sortOrder: f.sortOrder })),
...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)
}, [])
const setNormalizedDropIndicator = useCallback(
(indicator: DropIndicator | null) => {
if (!indicator || indicator.position !== 'after' || indicator.targetId === 'root') {
setDropIndicator(indicator)
return
}
const siblings = getSiblingItems(indicator.folderId)
const currentIdx = siblings.findIndex((s) => s.id === indicator.targetId)
const nextSibling = siblings[currentIdx + 1]
if (nextSibling) {
setDropIndicator({
targetId: nextSibling.id,
position: 'before',
folderId: indicator.folderId,
})
} else {
setDropIndicator(indicator)
}
},
[getSiblingItems]
)
const isNoOpMove = useCallback(
(
indicator: DropIndicator,
draggedIds: string[],
draggedType: 'folder' | 'workflow',
destinationFolderId: string | null,
currentFolderId: string | null | undefined
): boolean => {
if (indicator.position !== 'inside' && draggedIds.includes(indicator.targetId)) {
return true
}
if (currentFolderId !== destinationFolderId) {
return false
}
const siblingItems = getSiblingItems(destinationFolderId)
const remaining = siblingItems.filter(
(item) => !(item.type === draggedType && draggedIds.includes(item.id))
)
const insertAt = calculateInsertIndex(remaining, indicator)
const originalIdx = siblingItems.findIndex(
(item) => item.type === draggedType && item.id === draggedIds[0]
)
return insertAt === originalIdx
},
[getSiblingItems, calculateInsertIndex]
)
const handleWorkflowDrop = useCallback(
async (workflowIds: string[], indicator: DropIndicator) => {
if (!workflowIds.length || !workspaceId) return
try {
const destinationFolderId =
indicator.position === 'inside'
? indicator.targetId === 'root'
? null
: indicator.targetId
: indicator.folderId
type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number }
const currentFolders = useFolderStore.getState().folders
const destinationFolderId = getDestinationFolderId(indicator)
const currentWorkflows = useWorkflowRegistry.getState().workflows
const siblingFolders = Object.values(currentFolders).filter(
(f) => f.parentId === destinationFolderId
)
const siblingWorkflows = Object.values(currentWorkflows).filter(
(w) => w.folderId === destinationFolderId
)
const firstWorkflow = currentWorkflows[workflowIds[0]]
const siblingItems: SiblingItem[] = [
...siblingFolders.map((f) => ({
type: 'folder' as const,
id: f.id,
sortOrder: f.sortOrder,
})),
...siblingWorkflows.map((w) => ({
type: 'workflow' as const,
id: w.id,
sortOrder: w.sortOrder,
})),
].sort((a, b) => a.sortOrder - b.sortOrder)
if (
isNoOpMove(
indicator,
workflowIds,
'workflow',
destinationFolderId,
firstWorkflow?.folderId
)
) {
return
}
const siblingItems = getSiblingItems(destinationFolderId)
const movingSet = new Set(workflowIds)
const remaining = siblingItems.filter(
(item) => !(item.type === 'workflow' && movingSet.has(item.id))
)
const moving = workflowIds
.map((id) => {
const w = currentWorkflows[id]
return { type: 'workflow' as const, id, sortOrder: w?.sortOrder ?? 0 }
})
.map((id) => ({
type: 'workflow' as const,
id,
sortOrder: currentWorkflows[id]?.sortOrder ?? 0,
}))
.sort((a, b) => a.sortOrder - b.sortOrder)
let insertAt: number
if (indicator.position === 'inside') {
insertAt = remaining.length
} else {
const targetIdx = remaining.findIndex((item) => item.id === indicator.targetId)
insertAt = indicator.position === 'before' ? targetIdx : targetIdx + 1
}
const insertAt = calculateInsertIndex(remaining, indicator)
const newOrder: SiblingItem[] = [
...remaining.slice(0, insertAt),
@@ -179,35 +284,18 @@ export function useDragDrop() {
...remaining.slice(insertAt),
]
const folderUpdates = newOrder
.map((item, i) => ({ ...item, sortOrder: i }))
.filter((item) => item.type === 'folder')
.map((item) => ({
id: item.id,
sortOrder: item.sortOrder,
parentId: destinationFolderId,
}))
const workflowUpdates = newOrder
.map((item, i) => ({ ...item, sortOrder: i }))
.filter((item) => item.type === 'workflow')
.map((item) => ({
id: item.id,
sortOrder: item.sortOrder,
folderId: destinationFolderId,
}))
await Promise.all([
folderUpdates.length > 0 &&
reorderFoldersMutation.mutateAsync({ workspaceId, updates: folderUpdates }),
workflowUpdates.length > 0 &&
reorderWorkflowsMutation.mutateAsync({ workspaceId, updates: workflowUpdates }),
])
await buildAndSubmitUpdates(newOrder, destinationFolderId)
} catch (error) {
logger.error('Failed to reorder workflows:', error)
}
},
[workspaceId, reorderFoldersMutation, reorderWorkflowsMutation]
[
getDestinationFolderId,
getSiblingItems,
calculateInsertIndex,
isNoOpMove,
buildAndSubmitUpdates,
]
)
const handleFolderDrop = useCallback(
@@ -218,12 +306,7 @@ export function useDragDrop() {
const folderStore = useFolderStore.getState()
const currentFolders = folderStore.folders
const targetParentId =
indicator.position === 'inside'
? indicator.targetId === 'root'
? null
: indicator.targetId
: indicator.folderId
const targetParentId = getDestinationFolderId(indicator)
if (draggedFolderId === targetParentId) {
logger.info('Cannot move folder into itself')
@@ -238,39 +321,25 @@ export function useDragDrop() {
}
}
type SiblingItem = { type: 'folder' | 'workflow'; id: string; sortOrder: number }
const currentWorkflows = useWorkflowRegistry.getState().workflows
const siblingFolders = Object.values(currentFolders).filter(
(f) => f.parentId === targetParentId
)
const siblingWorkflows = Object.values(currentWorkflows).filter(
(w) => w.folderId === targetParentId
)
const siblingItems: SiblingItem[] = [
...siblingFolders.map((f) => ({
type: 'folder' as const,
id: f.id,
sortOrder: f.sortOrder,
})),
...siblingWorkflows.map((w) => ({
type: 'workflow' as const,
id: w.id,
sortOrder: w.sortOrder,
})),
].sort((a, b) => a.sortOrder - b.sortOrder)
const draggedFolder = currentFolders[draggedFolderId]
if (
isNoOpMove(
indicator,
[draggedFolderId],
'folder',
targetParentId,
draggedFolder?.parentId
)
) {
return
}
const siblingItems = getSiblingItems(targetParentId)
const remaining = siblingItems.filter(
(item) => !(item.type === 'folder' && item.id === draggedFolderId)
)
let insertAt: number
if (indicator.position === 'inside') {
insertAt = remaining.length
} else {
const targetIdx = remaining.findIndex((item) => item.id === indicator.targetId)
insertAt = indicator.position === 'before' ? targetIdx : targetIdx + 1
}
const insertAt = calculateInsertIndex(remaining, indicator)
const newOrder: SiblingItem[] = [
...remaining.slice(0, insertAt),
@@ -278,27 +347,19 @@ export function useDragDrop() {
...remaining.slice(insertAt),
]
const folderUpdates = newOrder
.map((item, i) => ({ ...item, sortOrder: i }))
.filter((item) => item.type === 'folder')
.map((item) => ({ id: item.id, sortOrder: item.sortOrder, parentId: targetParentId }))
const workflowUpdates = newOrder
.map((item, i) => ({ ...item, sortOrder: i }))
.filter((item) => item.type === 'workflow')
.map((item) => ({ id: item.id, sortOrder: item.sortOrder, folderId: targetParentId }))
await Promise.all([
folderUpdates.length > 0 &&
reorderFoldersMutation.mutateAsync({ workspaceId, updates: folderUpdates }),
workflowUpdates.length > 0 &&
reorderWorkflowsMutation.mutateAsync({ workspaceId, updates: workflowUpdates }),
])
await buildAndSubmitUpdates(newOrder, targetParentId)
} catch (error) {
logger.error('Failed to reorder folder:', error)
}
},
[workspaceId, reorderFoldersMutation, reorderWorkflowsMutation]
[
workspaceId,
getDestinationFolderId,
getSiblingItems,
calculateInsertIndex,
isNoOpMove,
buildAndSubmitUpdates,
]
)
const handleDrop = useCallback(
@@ -334,90 +395,98 @@ export function useDragDrop() {
const createWorkflowDragHandlers = useCallback(
(workflowId: string, folderId: string | null) => ({
onDragOver: (e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
lastDragYRef.current = e.clientY
setIsDragging(true)
const position = calculateDropPosition(e, e.currentTarget)
setDropIndicator({ targetId: workflowId, position, folderId })
initDragOver(e)
const isSameFolder = draggedSourceFolderRef.current === folderId
if (isSameFolder) {
const position = calculateDropPosition(e, e.currentTarget)
setNormalizedDropIndicator({ targetId: workflowId, position, folderId })
} else {
setNormalizedDropIndicator({
targetId: folderId || 'root',
position: 'inside',
folderId: null,
})
}
},
onDrop: handleDrop,
}),
[calculateDropPosition, handleDrop]
[initDragOver, calculateDropPosition, setNormalizedDropIndicator, handleDrop]
)
const createFolderDragHandlers = useCallback(
(folderId: string, parentFolderId: string | null) => ({
onDragOver: (e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
lastDragYRef.current = e.clientY
setIsDragging(true)
initDragOver(e)
if (draggedTypeRef.current === 'folder') {
const position = calculateDropPosition(e, e.currentTarget)
setDropIndicator({ targetId: folderId, position, folderId: parentFolderId })
const isSameParent = draggedSourceFolderRef.current === parentFolderId
if (isSameParent) {
const position = calculateDropPosition(e, e.currentTarget)
setNormalizedDropIndicator({ targetId: folderId, position, folderId: parentFolderId })
} else {
// Cross-container: highlight this folder (drop into it)
setNormalizedDropIndicator({
targetId: folderId,
position: 'inside',
folderId: parentFolderId,
})
setHoverFolderId(folderId)
}
} else {
setDropIndicator({ targetId: folderId, position: 'inside', folderId: parentFolderId })
setNormalizedDropIndicator({
targetId: folderId,
position: 'inside',
folderId: parentFolderId,
})
setHoverFolderId(folderId)
}
},
onDragLeave: (e: React.DragEvent<HTMLElement>) => {
const relatedTarget = e.relatedTarget as HTMLElement | null
const currentTarget = e.currentTarget as HTMLElement
if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
setHoverFolderId(null)
}
if (isLeavingElement(e)) setHoverFolderId(null)
},
onDrop: handleDrop,
}),
[calculateDropPosition, handleDrop]
[initDragOver, calculateDropPosition, setNormalizedDropIndicator, isLeavingElement, handleDrop]
)
const createEmptyFolderDropZone = useCallback(
(folderId: string) => ({
onDragOver: (e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
e.stopPropagation()
lastDragYRef.current = e.clientY
setIsDragging(true)
setDropIndicator({ targetId: folderId, position: 'inside', folderId })
initDragOver(e)
setNormalizedDropIndicator({ targetId: folderId, position: 'inside', folderId })
},
onDrop: handleDrop,
}),
[handleDrop]
[initDragOver, setNormalizedDropIndicator, handleDrop]
)
const createRootDropZone = useCallback(
() => ({
onDragOver: (e: React.DragEvent<HTMLElement>) => {
e.preventDefault()
lastDragYRef.current = e.clientY
setIsDragging(true)
setDropIndicator({ targetId: 'root', position: 'inside', folderId: null })
initDragOver(e, false)
setNormalizedDropIndicator({ targetId: 'root', position: 'inside', folderId: null })
},
onDragLeave: (e: React.DragEvent<HTMLElement>) => {
const relatedTarget = e.relatedTarget as HTMLElement | null
const currentTarget = e.currentTarget as HTMLElement
if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
setDropIndicator(null)
}
if (isLeavingElement(e)) setNormalizedDropIndicator(null)
},
onDrop: handleDrop,
}),
[handleDrop]
[initDragOver, setNormalizedDropIndicator, isLeavingElement, handleDrop]
)
const handleDragStart = useCallback((type: 'workflow' | 'folder') => {
draggedTypeRef.current = type
setIsDragging(true)
}, [])
const handleDragStart = useCallback(
(type: 'workflow' | 'folder', sourceFolderId: string | null) => {
draggedTypeRef.current = type
draggedSourceFolderRef.current = sourceFolderId
setIsDragging(true)
},
[]
)
const handleDragEnd = useCallback(() => {
setIsDragging(false)
setDropIndicator(null)
draggedTypeRef.current = null
draggedSourceFolderRef.current = null
setHoverFolderId(null)
}, [])

View File

@@ -140,9 +140,9 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
workspaceId,
})
const folderMap = new Map<string, string>()
const sanitizeName = (name: string) => name.replace(/[^a-z0-9-_]/gi, '-')
const exportedFoldersByPath = new Map<string, { sortOrder?: number }>()
if (metadata?.folders) {
if (metadata?.folders && metadata.folders.length > 0) {
type ExportedFolder = {
id: string
name: string
@@ -152,7 +152,7 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
const foldersById = new Map<string, ExportedFolder>(
metadata.folders.map((f) => [f.id, f])
)
const sanitizeName = (name: string) => name.replace(/[^a-z0-9-_]/gi, '-')
const oldIdToNewId = new Map<string, string>()
const buildPath = (folderId: string): string => {
const pathParts: string[] = []
@@ -165,9 +165,29 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
return pathParts.join('/')
}
for (const f of metadata.folders) {
const path = buildPath(f.id)
exportedFoldersByPath.set(path, { sortOrder: f.sortOrder })
const createFolderRecursive = async (folder: ExportedFolder): Promise<string> => {
if (oldIdToNewId.has(folder.id)) {
return oldIdToNewId.get(folder.id)!
}
let parentId = importFolder.id
if (folder.parentId && foldersById.has(folder.parentId)) {
parentId = await createFolderRecursive(foldersById.get(folder.parentId)!)
}
const newFolder = await createFolderMutation.mutateAsync({
name: folder.name,
workspaceId,
parentId,
sortOrder: folder.sortOrder,
})
oldIdToNewId.set(folder.id, newFolder.id)
folderMap.set(buildPath(folder.id), newFolder.id)
return newFolder.id
}
for (const folder of metadata.folders) {
await createFolderRecursive(folder)
}
}
@@ -178,20 +198,19 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
if (workflow.folderPath.length > 0) {
const folderPathKey = workflow.folderPath.join('/')
if (!folderMap.has(folderPathKey)) {
if (folderMap.has(folderPathKey)) {
targetFolderId = folderMap.get(folderPathKey)!
} else {
let parentId = importFolder.id
for (let i = 0; i < workflow.folderPath.length; i++) {
const pathSegment = workflow.folderPath.slice(0, i + 1).join('/')
const folderNameForSegment = workflow.folderPath[i]
if (!folderMap.has(pathSegment)) {
const exportedFolder = exportedFoldersByPath.get(pathSegment)
const subFolder = await createFolderMutation.mutateAsync({
name: folderNameForSegment,
workspaceId,
parentId,
sortOrder: exportedFolder?.sortOrder,
})
folderMap.set(pathSegment, subFolder.id)
parentId = subFolder.id
@@ -199,9 +218,8 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) {
parentId = folderMap.get(pathSegment)!
}
}
targetFolderId = folderMap.get(folderPathKey)!
}
targetFolderId = folderMap.get(folderPathKey)!
}
const workflowId = await importSingleWorkflow(

View File

@@ -59,7 +59,7 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {})
const createResponse = await fetch('/api/workspaces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: workspaceName }),
body: JSON.stringify({ name: workspaceName, skipDefaultWorkflow: true }),
})
if (!createResponse.ok) {
@@ -70,6 +70,56 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {})
logger.info('Created new workspace:', newWorkspace)
const folderMap = new Map<string, string>()
const sanitizeName = (name: string) => name.replace(/[^a-z0-9-_]/gi, '-')
if (metadata?.folders && metadata.folders.length > 0) {
type ExportedFolder = {
id: string
name: string
parentId: string | null
sortOrder?: number
}
const foldersById = new Map<string, ExportedFolder>(
metadata.folders.map((f) => [f.id, f])
)
const oldIdToNewId = new Map<string, string>()
const buildPath = (folderId: string): string => {
const pathParts: string[] = []
let currentId: string | null = folderId
while (currentId && foldersById.has(currentId)) {
const folder: ExportedFolder = foldersById.get(currentId)!
pathParts.unshift(sanitizeName(folder.name))
currentId = folder.parentId
}
return pathParts.join('/')
}
const createFolderRecursive = async (folder: ExportedFolder): Promise<string> => {
if (oldIdToNewId.has(folder.id)) {
return oldIdToNewId.get(folder.id)!
}
let parentId: string | undefined
if (folder.parentId && foldersById.has(folder.parentId)) {
parentId = await createFolderRecursive(foldersById.get(folder.parentId)!)
}
const newFolder = await createFolderMutation.mutateAsync({
name: folder.name,
workspaceId: newWorkspace.id,
parentId,
sortOrder: folder.sortOrder,
})
oldIdToNewId.set(folder.id, newFolder.id)
folderMap.set(buildPath(folder.id), newFolder.id)
return newFolder.id
}
for (const folder of metadata.folders) {
await createFolderRecursive(folder)
}
}
for (const workflow of extractedWorkflows) {
try {
@@ -84,9 +134,10 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {})
if (workflow.folderPath.length > 0) {
const folderPathKey = workflow.folderPath.join('/')
if (!folderMap.has(folderPathKey)) {
let parentId: string | null = null
if (folderMap.has(folderPathKey)) {
targetFolderId = folderMap.get(folderPathKey)!
} else {
let parentId: string | undefined
for (let i = 0; i < workflow.folderPath.length; i++) {
const pathSegment = workflow.folderPath.slice(0, i + 1).join('/')
@@ -94,7 +145,7 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {})
const subFolder = await createFolderMutation.mutateAsync({
name: workflow.folderPath[i],
workspaceId: newWorkspace.id,
parentId: parentId || undefined,
parentId,
})
folderMap.set(pathSegment, subFolder.id)
parentId = subFolder.id
@@ -102,9 +153,8 @@ export function useImportWorkspace({ onSuccess }: UseImportWorkspaceProps = {})
parentId = folderMap.get(pathSegment)!
}
}
targetFolderId = folderMap.get(folderPathKey) || null
}
targetFolderId = folderMap.get(folderPathKey) || null
}
const workflowName = extractWorkflowName(workflow.content, workflow.name)