This commit is contained in:
Siddharth Ganesan
2026-01-13 19:08:28 -08:00
parent 8ec067d280
commit a3007d8980
5 changed files with 92 additions and 67 deletions

View File

@@ -794,11 +794,14 @@ const WorkflowContent = React.memo(() => {
}, [contextMenuBlocks, collaborativeGroupBlocks])
const handleContextUngroupBlocks = useCallback(() => {
// Find the first block with a groupId and ungroup that entire group
// Find the first block with a groupId
const groupedBlock = contextMenuBlocks.find((block) => block.groupId)
if (groupedBlock?.groupId) {
collaborativeUngroupBlocks(groupedBlock.groupId)
}
if (!groupedBlock?.groupId) return
// The block's groupId is the group we want to ungroup
// This is the direct group the block belongs to, which is the "top level" from the user's perspective
// (the most recently created group that contains this block)
collaborativeUngroupBlocks(groupedBlock.groupId)
}, [contextMenuBlocks, collaborativeUngroupBlocks])
const handleContextRemoveFromSubflow = useCallback(() => {

View File

@@ -819,6 +819,7 @@ async function handleBlocksOperationTx(
logger.info(`Grouping ${blockIds.length} blocks into group ${groupId} in workflow ${workflowId}`)
// Update blocks: set groupId and push to groupStack
for (const blockId of blockIds) {
const [currentBlock] = await tx
.select({ data: workflowBlocks.data })
@@ -831,8 +832,13 @@ async function handleBlocksOperationTx(
continue
}
const currentData = currentBlock?.data || {}
const updatedData = { ...currentData, groupId }
const currentData = (currentBlock?.data || {}) as Record<string, any>
const currentStack = Array.isArray(currentData.groupStack) ? currentData.groupStack : []
const updatedData = {
...currentData,
groupId,
groupStack: [...currentStack, groupId],
}
await tx
.update(workflowBlocks)
@@ -848,7 +854,7 @@ async function handleBlocksOperationTx(
}
case BLOCKS_OPERATIONS.UNGROUP_BLOCKS: {
const { groupId, blockIds, parentGroupId } = payload
const { groupId, blockIds } = payload
if (!groupId || !Array.isArray(blockIds)) {
logger.debug('Invalid payload for ungroup blocks operation')
return
@@ -856,6 +862,7 @@ async function handleBlocksOperationTx(
logger.info(`Ungrouping ${blockIds.length} blocks from group ${groupId} in workflow ${workflowId}`)
// Update blocks: pop from groupStack and set groupId to the previous level
for (const blockId of blockIds) {
const [currentBlock] = await tx
.select({ data: workflowBlocks.data })
@@ -868,15 +875,23 @@ async function handleBlocksOperationTx(
continue
}
const currentData = currentBlock?.data || {}
let updatedData: Record<string, any>
const currentData = (currentBlock?.data || {}) as Record<string, any>
const currentStack = Array.isArray(currentData.groupStack) ? [...currentData.groupStack] : []
if (parentGroupId) {
// Move to parent group
updatedData = { ...currentData, groupId: parentGroupId }
// Pop the current groupId from the stack
if (currentStack.length > 0 && currentStack[currentStack.length - 1] === groupId) {
currentStack.pop()
}
// The new groupId is the top of the remaining stack, or undefined if empty
const newGroupId = currentStack.length > 0 ? currentStack[currentStack.length - 1] : undefined
let updatedData: Record<string, any>
if (newGroupId) {
updatedData = { ...currentData, groupId: newGroupId, groupStack: currentStack }
} else {
// Remove from group entirely
const { groupId: _removed, ...restData } = currentData
// Remove groupId and groupStack if stack is empty
const { groupId: _removed, groupStack: _removedStack, ...restData } = currentData
updatedData = restData
}

View File

@@ -298,11 +298,26 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
let workflowState: any
if (workflowData?.state) {
const blocks = workflowData.state.blocks || {}
// Reconstruct groups from blocks' groupId data
const reconstructedGroups: Record<string, { id: string; blockIds: string[] }> = {}
Object.entries(blocks).forEach(([blockId, block]: [string, any]) => {
const groupId = block?.data?.groupId
if (groupId) {
if (!reconstructedGroups[groupId]) {
reconstructedGroups[groupId] = { id: groupId, blockIds: [] }
}
reconstructedGroups[groupId].blockIds.push(blockId)
}
})
workflowState = {
blocks: workflowData.state.blocks || {},
blocks,
edges: workflowData.state.edges || [],
loops: workflowData.state.loops || {},
parallels: workflowData.state.parallels || {},
groups: reconstructedGroups,
lastSaved: Date.now(),
deploymentStatuses: {},
}
@@ -312,6 +327,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
edges: [],
loops: {},
parallels: {},
groups: {},
deploymentStatuses: {},
lastSaved: Date.now(),
}

View File

@@ -1345,34 +1345,25 @@ export const useWorkflowStore = create<WorkflowStore>()(
const currentGroups = get().groups || {}
const currentBlocks = get().blocks
// Remove blocks from any existing groups they might be in
// Create the new group with all selected block IDs
const updatedGroups = { ...currentGroups }
for (const gId of Object.keys(updatedGroups)) {
updatedGroups[gId] = {
...updatedGroups[gId],
blockIds: updatedGroups[gId].blockIds.filter((bid) => !blockIds.includes(bid)),
}
// Remove empty groups
if (updatedGroups[gId].blockIds.length === 0) {
delete updatedGroups[gId]
}
}
// Create the new group
updatedGroups[newGroupId] = {
id: newGroupId,
blockIds: [...blockIds],
}
// Update blocks with the new groupId
// Update blocks: set groupId and push to groupStack
const newBlocks = { ...currentBlocks }
for (const blockId of blockIds) {
if (newBlocks[blockId]) {
const currentData = newBlocks[blockId].data || {}
const currentStack = Array.isArray(currentData.groupStack) ? currentData.groupStack : []
newBlocks[blockId] = {
...newBlocks[blockId],
data: {
...newBlocks[blockId].data,
...currentData,
groupId: newGroupId,
groupStack: [...currentStack, newGroupId],
},
}
}
@@ -1384,7 +1375,10 @@ export const useWorkflowStore = create<WorkflowStore>()(
})
get().updateLastSaved()
logger.info('Created block group', { groupId: newGroupId, blockCount: blockIds.length })
logger.info('Created block group', {
groupId: newGroupId,
blockCount: blockIds.length,
})
return newGroupId
},
@@ -1399,68 +1393,65 @@ export const useWorkflowStore = create<WorkflowStore>()(
}
const blockIds = [...group.blockIds]
const parentGroupId = group.parentGroupId
// Remove the group
// Remove the group from the groups record
const updatedGroups = { ...currentGroups }
delete updatedGroups[groupId]
// Update blocks - remove groupId or assign to parent group
// Update blocks: pop from groupStack and set groupId to the previous level
const newBlocks = { ...currentBlocks }
for (const blockId of blockIds) {
if (newBlocks[blockId]) {
const newData = { ...newBlocks[blockId].data }
if (parentGroupId) {
newData.groupId = parentGroupId
} else {
delete newData.groupId
const currentData = { ...newBlocks[blockId].data }
const currentStack = Array.isArray(currentData.groupStack)
? [...currentData.groupStack]
: []
// Pop the current groupId from the stack
if (currentStack.length > 0 && currentStack[currentStack.length - 1] === groupId) {
currentStack.pop()
}
// The new groupId is the top of the remaining stack, or undefined if empty
const newGroupId =
currentStack.length > 0 ? currentStack[currentStack.length - 1] : undefined
if (newGroupId) {
currentData.groupId = newGroupId
currentData.groupStack = currentStack
} else {
// Remove groupId and groupStack if stack is empty
delete currentData.groupId
delete currentData.groupStack
}
newBlocks[blockId] = {
...newBlocks[blockId],
data: newData,
data: currentData,
}
}
}
// If there's a parent group, add the blocks to it
if (parentGroupId && updatedGroups[parentGroupId]) {
updatedGroups[parentGroupId] = {
...updatedGroups[parentGroupId],
blockIds: [...updatedGroups[parentGroupId].blockIds, ...blockIds],
}
}
set({
blocks: newBlocks,
groups: updatedGroups,
})
get().updateLastSaved()
logger.info('Ungrouped blocks', { groupId, blockCount: blockIds.length })
logger.info('Ungrouped blocks', {
groupId,
blockCount: blockIds.length,
})
return blockIds
},
getGroupBlockIds: (groupId: string, recursive = false) => {
getGroupBlockIds: (groupId: string) => {
const groups = get().groups || {}
const group = groups[groupId]
if (!group) return []
if (!recursive) {
return [...group.blockIds]
}
// Recursively get all block IDs, including from nested groups
const allBlockIds: string[] = [...group.blockIds]
// Find child groups (groups whose parentGroupId is this group)
for (const g of Object.values(groups)) {
if (g.parentGroupId === groupId) {
allBlockIds.push(...get().getGroupBlockIds(g.id, true))
}
}
return allBlockIds
return [...group.blockIds]
},
getGroups: () => {

View File

@@ -66,6 +66,8 @@ export interface BlockData {
// Block group membership
groupId?: string
/** Stack of group IDs for hierarchical grouping (oldest to newest) */
groupStack?: string[]
}
export interface BlockLayoutState {
@@ -159,8 +161,6 @@ export interface BlockGroup {
name?: string
/** Block IDs that are direct members of this group */
blockIds: string[]
/** Parent group ID if this group is nested inside another group */
parentGroupId?: string
}
export interface DragStartPosition {