fix(duplicate): added isWide and advacnedMode to optimistic duplicate, persist collapsed subblock state (#847)

* fix(duplicate): added isWide and advacnedMode to optimistic duplicate, ensured it persists on client & server

* use collaborative set subblock value instead of doing it locally for collapsed subblocks

* cleamup
This commit is contained in:
Waleed Latif
2025-08-01 17:42:31 -07:00
committed by GitHub
parent 63b4a81acc
commit 9f810e8c29
16 changed files with 720 additions and 27 deletions

View File

@@ -98,6 +98,7 @@ export const sampleWorkflowState = {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 95,
},
'agent-id': {
@@ -125,6 +126,7 @@ export const sampleWorkflowState = {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 680,
},
},

View File

@@ -175,6 +175,7 @@ export async function POST(request: NextRequest) {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 0,
data: block.data || {},
}

View File

@@ -283,6 +283,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 0,
data: block.data || {},
}
@@ -325,6 +326,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 0,
data: block.data || {},
}

View File

@@ -129,6 +129,7 @@ export async function POST(req: NextRequest) {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 95,
},
},
@@ -176,6 +177,7 @@ export async function POST(req: NextRequest) {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: '95',
subBlocks: {
startWorkflow: {

View File

@@ -161,6 +161,7 @@ async function createWorkspace(userId: string, name: string) {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 95,
},
},
@@ -206,6 +207,7 @@ async function createWorkspace(userId: string, name: string) {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: '95',
subBlocks: {
startWorkflow: {

View File

@@ -14,6 +14,7 @@ import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
import type { GenerationType } from '@/blocks/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -96,7 +97,11 @@ export function Code({
const collapsedStateKey = `${subBlockId}_collapsed`
const isCollapsed =
(useSubBlockStore((state) => state.getValue(blockId, collapsedStateKey)) as boolean) ?? false
const setCollapsedValue = useSubBlockStore((state) => state.setValue)
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const setCollapsedValue = (blockId: string, subblockId: string, value: any) => {
collaborativeSetSubblockValue(blockId, subblockId, value)
}
const showCollapseButton =
(subBlockId === 'responseFormat' || subBlockId === 'code') && code.split('\n').length > 5

View File

@@ -148,7 +148,7 @@ export function useCollaborativeWorkflow() {
workflowStore.setBlockWide(payload.id, payload.isWide)
break
case 'update-advanced-mode':
workflowStore.toggleBlockAdvancedMode(payload.id)
workflowStore.setBlockAdvancedMode(payload.id, payload.advancedMode)
break
case 'toggle-handles': {
const currentBlock = workflowStore.blocks[payload.id]
@@ -433,6 +433,7 @@ export function useCollaborativeWorkflow() {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 0,
parentId,
extent,
@@ -504,6 +505,7 @@ export function useCollaborativeWorkflow() {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 0, // Default height, will be set by the UI
parentId,
extent,
@@ -811,6 +813,7 @@ export function useCollaborativeWorkflow() {
enabled: sourceBlock.enabled ?? true,
horizontalHandles: sourceBlock.horizontalHandles ?? true,
isWide: sourceBlock.isWide ?? false,
advancedMode: sourceBlock.advancedMode ?? false,
height: sourceBlock.height || 0,
}
@@ -821,7 +824,14 @@ export function useCollaborativeWorkflow() {
offsetPosition,
sourceBlock.data ? JSON.parse(JSON.stringify(sourceBlock.data)) : {},
sourceBlock.data?.parentId,
sourceBlock.data?.extent
sourceBlock.data?.extent,
{
enabled: sourceBlock.enabled,
horizontalHandles: sourceBlock.horizontalHandles,
isWide: sourceBlock.isWide,
advancedMode: sourceBlock.advancedMode,
height: sourceBlock.height,
}
)
executeQueuedOperation('duplicate', 'block', duplicatedBlockData, () => {
@@ -830,7 +840,16 @@ export function useCollaborativeWorkflow() {
sourceBlock.type,
newName,
offsetPosition,
sourceBlock.data ? JSON.parse(JSON.stringify(sourceBlock.data)) : {}
sourceBlock.data ? JSON.parse(JSON.stringify(sourceBlock.data)) : {},
sourceBlock.data?.parentId,
sourceBlock.data?.extent,
{
enabled: sourceBlock.enabled,
horizontalHandles: sourceBlock.horizontalHandles,
isWide: sourceBlock.isWide,
advancedMode: sourceBlock.advancedMode,
height: sourceBlock.height,
}
)
// Apply subblock values locally for immediate UI feedback

View File

@@ -9,7 +9,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
// Mock database operations
const mockDb = {
select: vi.fn(),
insert: vi.fn(),
@@ -17,7 +16,6 @@ const mockDb = {
transaction: vi.fn(),
}
// Mock schema objects
const mockWorkflowBlocks = {
workflowId: 'workflowId',
id: 'id',
@@ -52,7 +50,6 @@ const mockWorkflowSubflows = {
config: 'config',
}
// Setup mocks before running tests
vi.doMock('@/db', () => ({
db: mockDb,
}))
@@ -76,7 +73,6 @@ vi.doMock('@/lib/logs/console/logger', () => ({
})),
}))
// Test data
const mockWorkflowId = 'test-workflow-123'
const mockBlocksFromDb = [
@@ -91,7 +87,7 @@ const mockBlocksFromDb = [
horizontalHandles: true,
isWide: false,
height: 150,
subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } },
subBlocks: { input: { id: 'input', type: 'short-input' as const, value: 'test' } },
outputs: { result: { type: 'string' } },
data: { parentId: null, extent: null, width: 350 },
parentId: null,
@@ -158,7 +154,7 @@ const mockWorkflowState: WorkflowState = {
type: 'starter',
name: 'Start Block',
position: { x: 100, y: 100 },
subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } },
subBlocks: { input: { id: 'input', type: 'short-input' as const, value: 'test' } },
outputs: { result: { type: 'string' } },
enabled: true,
horizontalHandles: true,
@@ -215,7 +211,6 @@ describe('Database Helpers', () => {
beforeEach(async () => {
vi.clearAllMocks()
// Import the module after mocks are set up
dbHelpers = await import('@/lib/workflows/db-helpers')
})
@@ -225,10 +220,8 @@ describe('Database Helpers', () => {
describe('loadWorkflowFromNormalizedTables', () => {
it('should successfully load workflow data from normalized tables', async () => {
// Reset all mocks first
vi.clearAllMocks()
// Mock each database query call separately since Promise.all makes 3 separate calls
let callCount = 0
mockDb.select.mockImplementation(() => ({
from: vi.fn().mockReturnValue({
@@ -266,7 +259,7 @@ describe('Database Helpers', () => {
horizontalHandles: true,
isWide: false,
height: 150,
subBlocks: { input: { id: 'input', type: 'short-input', value: 'test' } },
subBlocks: { input: { id: 'input', type: 'short-input' as const, value: 'test' } },
outputs: { result: { type: 'string' } },
data: { parentId: null, extent: null, width: 350 },
parentId: null,
@@ -777,4 +770,468 @@ describe('Database Helpers', () => {
expect(result.jsonBlob.edges).toHaveLength(999)
})
})
describe('advancedMode persistence comparison with isWide', () => {
it('should load advancedMode property exactly like isWide from database', async () => {
const testBlocks = [
{
id: 'block-advanced-wide',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Advanced Wide Block',
positionX: 100,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: true,
advancedMode: true,
height: 200,
subBlocks: {},
outputs: {},
data: {},
parentId: null,
extent: null,
},
{
id: 'block-basic-narrow',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Basic Narrow Block',
positionX: 200,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 150,
subBlocks: {},
outputs: {},
data: {},
parentId: null,
extent: null,
},
{
id: 'block-advanced-narrow',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Advanced Narrow Block',
positionX: 300,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: true,
height: 180,
subBlocks: {},
outputs: {},
data: {},
parentId: null,
extent: null,
},
]
vi.clearAllMocks()
let callCount = 0
mockDb.select.mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockImplementation(() => {
callCount++
if (callCount === 1) return Promise.resolve(testBlocks)
if (callCount === 2) return Promise.resolve([])
if (callCount === 3) return Promise.resolve([])
return Promise.resolve([])
}),
}),
}))
const result = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId)
expect(result).toBeDefined()
// Test all combinations of isWide and advancedMode
const advancedWideBlock = result?.blocks['block-advanced-wide']
expect(advancedWideBlock?.isWide).toBe(true)
expect(advancedWideBlock?.advancedMode).toBe(true)
const basicNarrowBlock = result?.blocks['block-basic-narrow']
expect(basicNarrowBlock?.isWide).toBe(false)
expect(basicNarrowBlock?.advancedMode).toBe(false)
const advancedNarrowBlock = result?.blocks['block-advanced-narrow']
expect(advancedNarrowBlock?.isWide).toBe(false)
expect(advancedNarrowBlock?.advancedMode).toBe(true)
})
it('should handle null/undefined advancedMode same way as isWide', async () => {
const blocksWithMissingProperties = [
{
id: 'block-null-props',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Block with null properties',
positionX: 100,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: null,
advancedMode: null,
height: 150,
subBlocks: {},
outputs: {},
data: {},
parentId: null,
extent: null,
},
{
id: 'block-undefined-props',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Block with undefined properties',
positionX: 200,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: undefined,
advancedMode: undefined,
height: 150,
subBlocks: {},
outputs: {},
data: {},
parentId: null,
extent: null,
},
]
vi.clearAllMocks()
let callCount = 0
mockDb.select.mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockImplementation(() => {
callCount++
if (callCount === 1) return Promise.resolve(blocksWithMissingProperties)
return Promise.resolve([])
}),
}),
}))
const result = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId)
expect(result).toBeDefined()
// Both isWide and advancedMode should handle null/undefined consistently
const nullPropsBlock = result?.blocks['block-null-props']
expect(nullPropsBlock?.isWide).toBeNull()
expect(nullPropsBlock?.advancedMode).toBeNull()
const undefinedPropsBlock = result?.blocks['block-undefined-props']
expect(undefinedPropsBlock?.isWide).toBeUndefined()
expect(undefinedPropsBlock?.advancedMode).toBeUndefined()
})
})
describe('end-to-end advancedMode persistence verification', () => {
it('should persist advancedMode through complete duplication and save cycle', async () => {
// Simulate the exact user workflow:
// 1. Create a block with advancedMode: true
// 2. Duplicate the block
// 3. Save workflow state (this was causing the bug)
// 4. Reload from database (simulate refresh)
// 5. Verify advancedMode is still true
const originalBlock = {
id: 'agent-original',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Agent 1',
positionX: 100,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: true,
advancedMode: true, // User sets this to advanced mode
height: 200,
subBlocks: {
systemPrompt: {
id: 'systemPrompt',
type: 'textarea',
value: 'You are a helpful assistant',
},
userPrompt: { id: 'userPrompt', type: 'textarea', value: 'Help the user' },
model: { id: 'model', type: 'select', value: 'gpt-4o' },
},
outputs: {},
data: {},
parentId: null,
extent: null,
}
const duplicatedBlock = {
id: 'agent-duplicate',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Agent 2',
positionX: 200,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: true,
advancedMode: true, // Should be copied from original
height: 200,
subBlocks: {
systemPrompt: {
id: 'systemPrompt',
type: 'textarea',
value: 'You are a helpful assistant',
},
userPrompt: { id: 'userPrompt', type: 'textarea', value: 'Help the user' },
model: { id: 'model', type: 'select', value: 'gpt-4o' },
},
outputs: {},
data: {},
parentId: null,
extent: null,
}
// Step 1 & 2: Mock loading both original and duplicated blocks from database
vi.clearAllMocks()
let callCount = 0
mockDb.select.mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockImplementation(() => {
callCount++
if (callCount === 1) return Promise.resolve([originalBlock, duplicatedBlock])
if (callCount === 2) return Promise.resolve([]) // edges
if (callCount === 3) return Promise.resolve([]) // subflows
return Promise.resolve([])
}),
}),
}))
// Step 3: Load workflow state (simulates app loading after duplication)
const loadedState = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId)
expect(loadedState).toBeDefined()
expect(loadedState?.blocks['agent-original'].advancedMode).toBe(true)
expect(loadedState?.blocks['agent-duplicate'].advancedMode).toBe(true)
// Step 4: Test the critical saveWorkflowToNormalizedTables function
// This was the function that was dropping advancedMode!
const workflowState = {
blocks: loadedState!.blocks,
edges: loadedState!.edges,
loops: {},
parallels: {},
deploymentStatuses: {},
hasActiveWebhook: false,
}
// Mock the transaction for save operation
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
const mockTx = {
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
insert: vi.fn().mockImplementation((_table) => ({
values: vi.fn().mockImplementation((values) => {
// Verify that advancedMode is included in the insert values
if (Array.isArray(values)) {
values.forEach((blockInsert) => {
if (blockInsert.id === 'agent-original') {
expect(blockInsert.advancedMode).toBe(true)
}
if (blockInsert.id === 'agent-duplicate') {
expect(blockInsert.advancedMode).toBe(true)
}
})
}
return Promise.resolve()
}),
})),
}
return await callback(mockTx)
})
mockDb.transaction = mockTransaction
// Step 5: Save workflow state (this should preserve advancedMode)
const saveResult = await dbHelpers.saveWorkflowToNormalizedTables(
mockWorkflowId,
workflowState
)
expect(saveResult.success).toBe(true)
// Step 6: Verify the JSON blob also preserves advancedMode
expect(saveResult.jsonBlob?.blocks['agent-original'].advancedMode).toBe(true)
expect(saveResult.jsonBlob?.blocks['agent-duplicate'].advancedMode).toBe(true)
// Verify the database insert was called with the correct values
expect(mockTransaction).toHaveBeenCalled()
})
it('should handle mixed advancedMode states correctly', async () => {
// Test scenario: one block in advanced mode, one in basic mode
const basicBlock = {
id: 'agent-basic',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Basic Agent',
positionX: 100,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false, // Basic mode
height: 150,
subBlocks: { model: { id: 'model', type: 'select', value: 'gpt-4o' } },
outputs: {},
data: {},
parentId: null,
extent: null,
}
const advancedBlock = {
id: 'agent-advanced',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Advanced Agent',
positionX: 200,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: true,
advancedMode: true, // Advanced mode
height: 200,
subBlocks: {
systemPrompt: { id: 'systemPrompt', type: 'textarea', value: 'System prompt' },
userPrompt: { id: 'userPrompt', type: 'textarea', value: 'User prompt' },
model: { id: 'model', type: 'select', value: 'gpt-4o' },
},
outputs: {},
data: {},
parentId: null,
extent: null,
}
vi.clearAllMocks()
let callCount = 0
mockDb.select.mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockImplementation(() => {
callCount++
if (callCount === 1) return Promise.resolve([basicBlock, advancedBlock])
return Promise.resolve([])
}),
}),
}))
const loadedState = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId)
expect(loadedState).toBeDefined()
// Verify mixed states are preserved
expect(loadedState?.blocks['agent-basic'].advancedMode).toBe(false)
expect(loadedState?.blocks['agent-advanced'].advancedMode).toBe(true)
// Verify other properties are also preserved correctly
expect(loadedState?.blocks['agent-basic'].isWide).toBe(false)
expect(loadedState?.blocks['agent-advanced'].isWide).toBe(true)
})
it('should preserve advancedMode during workflow state round-trip', async () => {
// Test the complete round-trip: save to DB → load from DB
const testWorkflowState = {
blocks: {
'block-1': {
id: 'block-1',
type: 'agent',
name: 'Test Agent',
position: { x: 100, y: 100 },
subBlocks: {
systemPrompt: { id: 'systemPrompt', type: 'long-input' as const, value: 'System' },
model: { id: 'model', type: 'dropdown' as const, value: 'gpt-4o' },
},
outputs: {},
enabled: true,
horizontalHandles: true,
isWide: true,
advancedMode: true,
height: 200,
data: {},
},
},
edges: [],
loops: {},
parallels: {},
deploymentStatuses: {},
hasActiveWebhook: false,
}
// Mock successful save
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
const mockTx = {
delete: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockResolvedValue(undefined),
}),
}
return await callback(mockTx)
})
mockDb.transaction = mockTransaction
// Save the state
const saveResult = await dbHelpers.saveWorkflowToNormalizedTables(
mockWorkflowId,
testWorkflowState
)
expect(saveResult.success).toBe(true)
// Mock loading the saved state back
vi.clearAllMocks()
let callCount = 0
mockDb.select.mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockImplementation(() => {
callCount++
if (callCount === 1) {
return Promise.resolve([
{
id: 'block-1',
workflowId: mockWorkflowId,
type: 'agent',
name: 'Test Agent',
positionX: 100,
positionY: 100,
enabled: true,
horizontalHandles: true,
isWide: true,
advancedMode: true, // This should be preserved
height: 200,
subBlocks: {
systemPrompt: { id: 'systemPrompt', type: 'textarea', value: 'System' },
model: { id: 'model', type: 'select', value: 'gpt-4o' },
},
outputs: {},
data: {},
parentId: null,
extent: null,
},
])
}
return Promise.resolve([])
}),
}),
}))
// Load the state back
const loadedState = await dbHelpers.loadWorkflowFromNormalizedTables(mockWorkflowId)
expect(loadedState).toBeDefined()
expect(loadedState?.blocks['block-1'].advancedMode).toBe(true)
expect(loadedState?.blocks['block-1'].isWide).toBe(true)
})
})
})

View File

@@ -146,6 +146,7 @@ export async function saveWorkflowToNormalizedTables(
enabled: block.enabled ?? true,
horizontalHandles: block.horizontalHandles ?? true,
isWide: block.isWide ?? false,
advancedMode: block.advancedMode ?? false,
height: String(block.height || 0),
subBlocks: block.subBlocks || {},
outputs: block.outputs || {},

View File

@@ -28,6 +28,7 @@ const testWorkflowState = {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 90,
},
'loop-block-456': {
@@ -43,6 +44,7 @@ const testWorkflowState = {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 0,
data: {
width: 400,
@@ -74,6 +76,7 @@ const testWorkflowState = {
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 144,
data: {
parentId: 'loop-block-456',

View File

@@ -129,6 +129,7 @@ async function migrateWorkflowStates(specificWorkflowIds?: string[] | null) {
enabled: block.enabled ?? true,
horizontalHandles: block.horizontalHandles ?? true,
isWide: block.isWide ?? false,
advancedMode: block.advancedMode ?? false,
height: String(block.height || 0),
subBlocks: block.subBlocks || {},
outputs: block.outputs || {},

View File

@@ -268,6 +268,7 @@ async function handleBlockOperationTx(
enabled: payload.enabled ?? true,
horizontalHandles: payload.horizontalHandles ?? true,
isWide: payload.isWide ?? false,
advancedMode: payload.advancedMode ?? false,
height: payload.height || 0,
}
@@ -617,6 +618,7 @@ async function handleBlockOperationTx(
enabled: payload.enabled ?? true,
horizontalHandles: payload.horizontalHandles ?? true,
isWide: payload.isWide ?? false,
advancedMode: payload.advancedMode ?? false,
height: payload.height || 0,
}

View File

@@ -730,6 +730,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 0,
}
@@ -1105,6 +1106,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
enabled: true,
horizontalHandles: true,
isWide: false,
advancedMode: false,
height: 0,
}

View File

@@ -299,9 +299,9 @@ describe('workflow store', () => {
// Add an agent block
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
// Initially should be in basic mode (advancedMode: false or undefined)
// Initially should be in basic mode (advancedMode: false)
let state = useWorkflowStore.getState()
expect(state.blocks.agent1?.advancedMode).toBeUndefined()
expect(state.blocks.agent1?.advancedMode).toBe(false)
// Toggle to advanced mode
toggleBlockAdvancedMode('agent1')
@@ -407,7 +407,7 @@ describe('workflow store', () => {
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
// Toggle modes without any subblock values set
expect(useWorkflowStore.getState().blocks.agent1?.advancedMode).toBeUndefined()
expect(useWorkflowStore.getState().blocks.agent1?.advancedMode).toBe(false)
expect(() => toggleBlockAdvancedMode('agent1')).not.toThrow()
// Verify the mode changed
@@ -422,4 +422,181 @@ describe('workflow store', () => {
expect(() => toggleBlockAdvancedMode('non-existent')).not.toThrow()
})
})
describe('addBlock with blockProperties', () => {
it('should create a block with default properties when no blockProperties provided', () => {
const { addBlock } = useWorkflowStore.getState()
addBlock('agent1', 'agent', 'Test Agent', { x: 100, y: 200 })
const state = useWorkflowStore.getState()
const block = state.blocks.agent1
expect(block).toBeDefined()
expect(block.id).toBe('agent1')
expect(block.type).toBe('agent')
expect(block.name).toBe('Test Agent')
expect(block.position).toEqual({ x: 100, y: 200 })
expect(block.enabled).toBe(true)
expect(block.horizontalHandles).toBe(true)
expect(block.isWide).toBe(false)
expect(block.height).toBe(0)
})
it('should create a block with custom blockProperties for regular blocks', () => {
const { addBlock } = useWorkflowStore.getState()
addBlock(
'agent1',
'agent',
'Test Agent',
{ x: 100, y: 200 },
{ someData: 'test' },
undefined,
undefined,
{
enabled: false,
horizontalHandles: false,
isWide: true,
advancedMode: true,
height: 300,
}
)
const state = useWorkflowStore.getState()
const block = state.blocks.agent1
expect(block).toBeDefined()
expect(block.enabled).toBe(false)
expect(block.horizontalHandles).toBe(false)
expect(block.isWide).toBe(true)
expect(block.advancedMode).toBe(true)
expect(block.height).toBe(300)
})
it('should create a loop block with custom blockProperties', () => {
const { addBlock } = useWorkflowStore.getState()
addBlock(
'loop1',
'loop',
'Test Loop',
{ x: 0, y: 0 },
{ loopType: 'for', count: 5 },
undefined,
undefined,
{
enabled: false,
horizontalHandles: false,
isWide: true,
advancedMode: true,
height: 250,
}
)
const state = useWorkflowStore.getState()
const block = state.blocks.loop1
expect(block).toBeDefined()
expect(block.enabled).toBe(false)
expect(block.horizontalHandles).toBe(false)
expect(block.isWide).toBe(true)
expect(block.advancedMode).toBe(true)
expect(block.height).toBe(250)
})
it('should create a parallel block with custom blockProperties', () => {
const { addBlock } = useWorkflowStore.getState()
addBlock(
'parallel1',
'parallel',
'Test Parallel',
{ x: 0, y: 0 },
{ count: 3 },
undefined,
undefined,
{
enabled: false,
horizontalHandles: false,
isWide: true,
advancedMode: true,
height: 400,
}
)
const state = useWorkflowStore.getState()
const block = state.blocks.parallel1
expect(block).toBeDefined()
expect(block.enabled).toBe(false)
expect(block.horizontalHandles).toBe(false)
expect(block.isWide).toBe(true)
expect(block.advancedMode).toBe(true)
expect(block.height).toBe(400)
})
it('should handle partial blockProperties (only some properties provided)', () => {
const { addBlock } = useWorkflowStore.getState()
addBlock(
'agent1',
'agent',
'Test Agent',
{ x: 100, y: 200 },
undefined,
undefined,
undefined,
{
isWide: true,
// Only isWide provided, others should use defaults
}
)
const state = useWorkflowStore.getState()
const block = state.blocks.agent1
expect(block).toBeDefined()
expect(block.enabled).toBe(true) // default
expect(block.horizontalHandles).toBe(true) // default
expect(block.isWide).toBe(true) // custom
expect(block.advancedMode).toBe(false) // default
expect(block.height).toBe(0) // default
})
it('should handle blockProperties with parent relationships', () => {
const { addBlock } = useWorkflowStore.getState()
// First add a parent loop block
addBlock('loop1', 'loop', 'Parent Loop', { x: 0, y: 0 })
// Then add a child block with custom properties
addBlock(
'agent1',
'agent',
'Child Agent',
{ x: 50, y: 50 },
{ parentId: 'loop1' },
'loop1',
'parent',
{
enabled: false,
isWide: true,
advancedMode: true,
height: 200,
}
)
const state = useWorkflowStore.getState()
const childBlock = state.blocks.agent1
expect(childBlock).toBeDefined()
expect(childBlock.enabled).toBe(false)
expect(childBlock.isWide).toBe(true)
expect(childBlock.advancedMode).toBe(true)
expect(childBlock.height).toBe(200)
expect(childBlock.data?.parentId).toBe('loop1')
expect(childBlock.data?.extent).toBe('parent')
})
})
})

View File

@@ -95,7 +95,14 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
position: Position,
data?: Record<string, any>,
parentId?: string,
extent?: 'parent'
extent?: 'parent',
blockProperties?: {
enabled?: boolean
horizontalHandles?: boolean
isWide?: boolean
advancedMode?: boolean
height?: number
}
) => {
const blockConfig = getBlock(type)
// For custom nodes like loop and parallel that don't use BlockConfig
@@ -116,10 +123,11 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
position,
subBlocks: {},
outputs: {},
enabled: true,
horizontalHandles: true,
isWide: false,
height: 0,
enabled: blockProperties?.enabled ?? true,
horizontalHandles: blockProperties?.horizontalHandles ?? true,
isWide: blockProperties?.isWide ?? false,
advancedMode: blockProperties?.advancedMode ?? false,
height: blockProperties?.height ?? 0,
data: nodeData,
},
},
@@ -165,10 +173,11 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
position,
subBlocks,
outputs,
enabled: true,
horizontalHandles: true,
isWide: false,
height: 0,
enabled: blockProperties?.enabled ?? true,
horizontalHandles: blockProperties?.horizontalHandles ?? true,
isWide: blockProperties?.isWide ?? false,
advancedMode: blockProperties?.advancedMode ?? false,
height: blockProperties?.height ?? 0,
data: nodeData,
},
},

View File

@@ -161,7 +161,14 @@ export interface WorkflowActions {
position: Position,
data?: Record<string, any>,
parentId?: string,
extent?: 'parent'
extent?: 'parent',
blockProperties?: {
enabled?: boolean
horizontalHandles?: boolean
isWide?: boolean
advancedMode?: boolean
height?: number
}
) => void
updateBlockPosition: (id: string, position: Position) => void
updateNodeDimensions: (id: string, dimensions: { width: number; height: number }) => void
@@ -177,6 +184,7 @@ export interface WorkflowActions {
updateBlockName: (id: string, name: string) => void
toggleBlockWide: (id: string) => void
setBlockWide: (id: string, isWide: boolean) => void
setBlockAdvancedMode: (id: string, advancedMode: boolean) => void
updateBlockHeight: (id: string, height: number) => void
triggerUpdate: () => void
updateLoopCount: (loopId: string, count: number) => void