mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
fix edge cases
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user