mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -175,6 +175,7 @@ export async function POST(request: NextRequest) {
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
height: 0,
|
||||
data: block.data || {},
|
||||
}
|
||||
|
||||
@@ -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 || {},
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 || {},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 || {},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user