mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(layout): layout canvas onInit after node sizes are calculated, remove legacy history code and legacy marking of dirty workflows (#1645)
This commit is contained in:
@@ -38,7 +38,7 @@ export default function StructuredData() {
|
||||
url: 'https://sim.ai',
|
||||
name: 'Sim - AI Agent Workflow Builder',
|
||||
description:
|
||||
'Open-source AI agent workflow builder. 30,000+ developers build and deploy agentic workflows. SOC2 and HIPAA compliant.',
|
||||
'Open-source AI agent workflow builder. 50,000+ developers build and deploy agentic workflows. SOC2 and HIPAA compliant.',
|
||||
publisher: {
|
||||
'@id': 'https://sim.ai/#organization',
|
||||
},
|
||||
@@ -98,7 +98,7 @@ export default function StructuredData() {
|
||||
'@id': 'https://sim.ai/#software',
|
||||
name: 'Sim - AI Agent Workflow Builder',
|
||||
description:
|
||||
'Open-source AI agent workflow builder used by 30,000+ developers. Build agentic workflows with visual drag-and-drop interface. SOC2 and HIPAA compliant. Integrate with 100+ apps.',
|
||||
'Open-source AI agent workflow builder used by 50,000+ developers. Build agentic workflows with visual drag-and-drop interface. SOC2 and HIPAA compliant. Integrate with 100+ apps.',
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
applicationSubCategory: 'AI Development Tools',
|
||||
operatingSystem: 'Web, Windows, macOS, Linux',
|
||||
@@ -173,7 +173,7 @@ export default function StructuredData() {
|
||||
'Visual workflow builder',
|
||||
'Drag-and-drop interface',
|
||||
'100+ integrations',
|
||||
'AI model support (OpenAI, Anthropic, Google)',
|
||||
'AI model support (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)',
|
||||
'Real-time collaboration',
|
||||
'Version control',
|
||||
'API access',
|
||||
@@ -198,7 +198,7 @@ export default function StructuredData() {
|
||||
name: 'What is Sim?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim is an open-source AI agent workflow builder used by 30,000+ developers at trail-blazing startups to Fortune 500 companies. It provides a visual drag-and-drop interface for building and deploying agentic workflows. Sim is SOC2 and HIPAA compliant.',
|
||||
text: 'Sim is an open-source AI agent workflow builder used by 50,000+ developers at trail-blazing startups to Fortune 500 companies. It provides a visual drag-and-drop interface for building and deploying agentic workflows. Sim is SOC2 and HIPAA compliant.',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -206,7 +206,7 @@ export default function StructuredData() {
|
||||
name: 'Which AI models does Sim support?',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'Sim supports all major AI models including OpenAI (GPT-4, GPT-3.5), Anthropic (Claude), Google (Gemini), Mistral, Perplexity, and many more. You can also connect to open-source models via Ollama.',
|
||||
text: 'Sim supports all major AI models including OpenAI (GPT-5, GPT-4o), Anthropic (Claude), Google (Gemini), xAI (Grok), Mistral, Perplexity, and many more. You can also connect to open-source models via Ollama.',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,7 +38,6 @@ export default function manifest(): MetadataRoute.Manifest {
|
||||
short_name: 'New',
|
||||
description: 'Create a new AI workflow',
|
||||
url: '/workspace',
|
||||
icons: [{ src: '/icons/new-workflow.png', sizes: '192x192' }],
|
||||
},
|
||||
],
|
||||
lang: 'en-US',
|
||||
|
||||
@@ -4,7 +4,7 @@ import Landing from '@/app/(landing)/landing'
|
||||
export const metadata: Metadata = {
|
||||
title: 'Sim - AI Agent Workflow Builder | Open Source Platform',
|
||||
description:
|
||||
'Open-source AI agent workflow builder used by 30,000+ developers. Build and deploy agentic workflows with visual drag-and-drop interface. Connect 100+ apps. SOC2 and HIPAA compliant. Used by startups to Fortune 500 companies.',
|
||||
'Open-source AI agent workflow builder used by 50,000+ developers. Build and deploy agentic workflows with visual drag-and-drop interface. Connect 100+ apps. SOC2 and HIPAA compliant. Used by startups to Fortune 500 companies.',
|
||||
keywords:
|
||||
'AI agent workflow builder, agentic workflows, open source AI, visual workflow builder, AI automation, LLM workflows, AI agents, workflow automation, no-code AI, SOC2 compliant, HIPAA compliant, enterprise AI',
|
||||
authors: [{ name: 'Sim Studio' }],
|
||||
@@ -18,7 +18,7 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
title: 'Sim - AI Agent Workflow Builder | Open Source',
|
||||
description:
|
||||
'Open-source platform used by 30,000+ developers. Build and deploy agentic workflows with drag-and-drop interface. SOC2 & HIPAA compliant. Connect 100+ apps.',
|
||||
'Open-source platform used by 50,000+ developers. Build and deploy agentic workflows with drag-and-drop interface. SOC2 & HIPAA compliant. Connect 100+ apps.',
|
||||
type: 'website',
|
||||
url: 'https://sim.ai',
|
||||
siteName: 'Sim',
|
||||
@@ -45,7 +45,7 @@ export const metadata: Metadata = {
|
||||
creator: '@simdotai',
|
||||
title: 'Sim - AI Agent Workflow Builder | Open Source',
|
||||
description:
|
||||
'Open-source platform for agentic workflows. 30,000+ developers. Visual builder. 100+ integrations. SOC2 & HIPAA compliant.',
|
||||
'Open-source platform for agentic workflows. 50,000+ developers. Visual builder. 100+ integrations. SOC2 & HIPAA compliant.',
|
||||
images: {
|
||||
url: '/social/twitter-image.png',
|
||||
alt: 'Sim - Visual AI Workflow Builder',
|
||||
|
||||
+1
-2
@@ -86,8 +86,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
// Store hooks
|
||||
const { history, revertToHistoryState, lastSaved, setNeedsRedeploymentFlag, blocks } =
|
||||
useWorkflowStore()
|
||||
const { lastSaved, setNeedsRedeploymentFlag, blocks } = useWorkflowStore()
|
||||
const {
|
||||
workflows,
|
||||
updateWorkflow,
|
||||
|
||||
@@ -92,7 +92,8 @@ const WorkflowContent = React.memo(() => {
|
||||
const [draggedNodeId, setDraggedNodeId] = useState<string | null>(null)
|
||||
const [potentialParentId, setPotentialParentId] = useState<string | null>(null)
|
||||
// State for tracking validation errors
|
||||
const [nestedSubflowErrors, setNestedSubflowErrors] = useState<Set<string>>(new Set())
|
||||
// Use a function initializer to ensure the Set is only created once
|
||||
const [nestedSubflowErrors, setNestedSubflowErrors] = useState<Set<string>>(() => new Set())
|
||||
// Enhanced edge selection with parent context and unique identifier
|
||||
const [selectedEdgeInfo, setSelectedEdgeInfo] = useState<SelectedEdgeInfo | null>(null)
|
||||
|
||||
@@ -1292,8 +1293,6 @@ const WorkflowContent = React.memo(() => {
|
||||
// Include dynamic dimensions for container resizing calculations
|
||||
width: block.isWide ? 450 : 350, // Standard width based on isWide state
|
||||
height: Math.max(block.height || 100, 100), // Use actual height with minimum
|
||||
// Explicitly set measured to prevent ReactFlow from recalculating
|
||||
measured: { width: block.isWide ? 450 : 350, height: Math.max(block.height || 100, 100) },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1967,7 +1966,13 @@ const WorkflowContent = React.memo(() => {
|
||||
edgeTypes={edgeTypes}
|
||||
onDrop={effectivePermissions.canEdit ? onDrop : undefined}
|
||||
onDragOver={effectivePermissions.canEdit ? onDragOver : undefined}
|
||||
fitView
|
||||
onInit={(instance) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
instance.fitView({ padding: 0.3 })
|
||||
})
|
||||
})
|
||||
}}
|
||||
minZoom={0.1}
|
||||
maxZoom={1.3}
|
||||
panOnScroll
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import type { WorkflowState, WorkflowStore } from '@/stores/workflows/workflow/types'
|
||||
|
||||
interface HistoryEntry {
|
||||
state: WorkflowState
|
||||
timestamp: number
|
||||
action: string
|
||||
subblockValues: Record<string, Record<string, any>>
|
||||
}
|
||||
|
||||
interface WorkflowHistory {
|
||||
past: HistoryEntry[]
|
||||
present: HistoryEntry
|
||||
future: HistoryEntry[]
|
||||
}
|
||||
|
||||
interface HistoryActions {
|
||||
undo: () => void
|
||||
redo: () => void
|
||||
canUndo: () => boolean
|
||||
canRedo: () => boolean
|
||||
revertToHistoryState: (index: number) => void
|
||||
}
|
||||
|
||||
// MAX for each individual workflow
|
||||
const MAX_HISTORY_LENGTH = 20
|
||||
|
||||
// Default empty state for consistent initialization
|
||||
const EMPTY_WORKFLOW_STATE = {
|
||||
blocks: {},
|
||||
edges: [] as any[],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
}
|
||||
|
||||
// Types for workflow store with history management capabilities
|
||||
export interface WorkflowStoreWithHistory extends WorkflowStore, HistoryActions {
|
||||
history: WorkflowHistory
|
||||
revertToDeployedState: (deployedState: WorkflowState) => void
|
||||
}
|
||||
|
||||
// Higher-order store middleware that adds undo/redo functionality
|
||||
export const withHistory = (
|
||||
config: StateCreator<WorkflowStoreWithHistory>
|
||||
): StateCreator<WorkflowStoreWithHistory> => {
|
||||
return (set, get, api) => {
|
||||
// Initialize store with history tracking
|
||||
const initialState = config(set, get, api)
|
||||
const initialHistoryEntry: HistoryEntry = {
|
||||
state: {
|
||||
blocks: initialState.blocks,
|
||||
edges: initialState.edges,
|
||||
loops: initialState.loops,
|
||||
parallels: initialState.parallels,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {}, // Add storage for subblock values
|
||||
}
|
||||
|
||||
return {
|
||||
...initialState,
|
||||
history: {
|
||||
past: [],
|
||||
present: initialHistoryEntry,
|
||||
future: [],
|
||||
},
|
||||
|
||||
// Check if undo operation is available
|
||||
canUndo: () => get().history.past.length > 0,
|
||||
|
||||
// Check if redo operation is available
|
||||
canRedo: () => get().history.future.length > 0,
|
||||
|
||||
// Restore previous state from history
|
||||
undo: () => {
|
||||
const { history, ...state } = get()
|
||||
if (history.past.length === 0) return
|
||||
|
||||
const previous = history.past[history.past.length - 1]
|
||||
const newPast = history.past.slice(0, history.past.length - 1)
|
||||
|
||||
// Get active workflow ID for subblock handling
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// Apply the state change
|
||||
set({
|
||||
...state,
|
||||
...previous.state,
|
||||
history: {
|
||||
past: newPast,
|
||||
present: previous,
|
||||
future: [history.present, ...history.future],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
|
||||
// Restore subblock values from the previous state's snapshot
|
||||
if (previous.subblockValues && activeWorkflowId) {
|
||||
// Update the subblock store with the saved values
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...useSubBlockStore.getState().workflowValues,
|
||||
[activeWorkflowId]: previous.subblockValues,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Restore next state from history
|
||||
redo: () => {
|
||||
const { history, ...state } = get()
|
||||
if (history.future.length === 0) return
|
||||
|
||||
const next = history.future[0]
|
||||
const newFuture = history.future.slice(1)
|
||||
|
||||
// Get active workflow ID for subblock handling
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
// Apply the state change
|
||||
set({
|
||||
...state,
|
||||
...next.state,
|
||||
history: {
|
||||
past: [...history.past, history.present],
|
||||
present: next,
|
||||
future: newFuture,
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
|
||||
// Restore subblock values from the next state's snapshot
|
||||
if (next.subblockValues && activeWorkflowId) {
|
||||
// Update the subblock store with the saved values
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...useSubBlockStore.getState().workflowValues,
|
||||
[activeWorkflowId]: next.subblockValues,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Reset workflow to empty state
|
||||
clear: () => {
|
||||
const newState = {
|
||||
...EMPTY_WORKFLOW_STATE,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: { ...EMPTY_WORKFLOW_STATE },
|
||||
timestamp: Date.now(),
|
||||
action: 'Clear workflow',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
set(newState)
|
||||
return newState
|
||||
},
|
||||
|
||||
// Jump to specific point in history
|
||||
revertToHistoryState: (index: number) => {
|
||||
const { history, ...state } = get()
|
||||
const allStates = [...history.past, history.present, ...history.future]
|
||||
const targetState = allStates[index]
|
||||
|
||||
if (!targetState) return
|
||||
|
||||
// Get active workflow ID for subblock handling
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (!activeWorkflowId) return
|
||||
|
||||
const newPast = allStates.slice(0, index)
|
||||
const newFuture = allStates.slice(index + 1)
|
||||
|
||||
set({
|
||||
...state,
|
||||
...targetState.state,
|
||||
history: {
|
||||
past: newPast,
|
||||
present: targetState,
|
||||
future: newFuture,
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
|
||||
// Restore subblock values from the target state's snapshot
|
||||
if (targetState.subblockValues && activeWorkflowId) {
|
||||
// Update the subblock store with the saved values
|
||||
useSubBlockStore.setState({
|
||||
workflowValues: {
|
||||
...useSubBlockStore.getState().workflowValues,
|
||||
[activeWorkflowId]: targetState.subblockValues,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new history entry with current state snapshot
|
||||
export const createHistoryEntry = (state: WorkflowState, action: string): HistoryEntry => {
|
||||
// Get active workflow ID for subblock handling
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
|
||||
// Create a deep copy of the state
|
||||
const stateCopy = {
|
||||
blocks: { ...state.blocks },
|
||||
edges: [...state.edges],
|
||||
loops: { ...state.loops },
|
||||
parallels: { ...state.parallels },
|
||||
}
|
||||
|
||||
// Capture the current subblock values for this workflow
|
||||
let subblockValues = {}
|
||||
|
||||
if (activeWorkflowId) {
|
||||
// Get the current subblock values from the store
|
||||
const currentValues = useSubBlockStore.getState().workflowValues[activeWorkflowId] || {}
|
||||
|
||||
// Create a deep copy to ensure we don't have reference issues
|
||||
subblockValues = JSON.parse(JSON.stringify(currentValues))
|
||||
}
|
||||
|
||||
return {
|
||||
state: stateCopy,
|
||||
timestamp: Date.now(),
|
||||
action,
|
||||
subblockValues,
|
||||
}
|
||||
}
|
||||
|
||||
// Add new entry to history and maintain history size limit
|
||||
export const pushHistory = (
|
||||
set: (
|
||||
partial:
|
||||
| Partial<WorkflowStoreWithHistory>
|
||||
| ((state: WorkflowStoreWithHistory) => Partial<WorkflowStoreWithHistory>),
|
||||
replace?: boolean
|
||||
) => void,
|
||||
get: () => WorkflowStoreWithHistory,
|
||||
newState: WorkflowState,
|
||||
action: string
|
||||
) => {
|
||||
const { history } = get()
|
||||
const newEntry = createHistoryEntry(newState, action)
|
||||
|
||||
set({
|
||||
history: {
|
||||
past: [...history.past, history.present].slice(-MAX_HISTORY_LENGTH),
|
||||
present: newEntry,
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
}
|
||||
@@ -90,7 +90,6 @@ async function fetchWorkflowsFromDB(workspaceId?: string): Promise<void> {
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
state,
|
||||
variables,
|
||||
createdAt,
|
||||
marketplaceData,
|
||||
@@ -186,26 +185,10 @@ function resetWorkflowStores() {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
deploymentStatuses: {}, // Reset deployment statuses map
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
|
||||
@@ -438,13 +421,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
|
||||
logger.info(`Switching to workflow ${id}`)
|
||||
|
||||
// First, sync the current workflow before switching (if there is one)
|
||||
if (activeWorkflowId && activeWorkflowId !== id) {
|
||||
// Mark current workflow as dirty and sync (fire and forget)
|
||||
useWorkflowStore.getState().sync.markDirty()
|
||||
useWorkflowStore.getState().sync.forceSync()
|
||||
}
|
||||
|
||||
// Fetch workflow state from database
|
||||
const response = await fetch(`/api/workflows/${id}`, { method: 'GET' })
|
||||
const workflowData = response.ok ? (await response.json()).data : null
|
||||
@@ -464,25 +440,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
lastSaved: Date.now(),
|
||||
marketplaceData: workflowData.marketplaceData || null,
|
||||
deploymentStatuses: {},
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: workflowData.state.blocks || {},
|
||||
edges: workflowData.state.edges || [],
|
||||
loops: workflowData.state.loops || {},
|
||||
parallels: workflowData.state.parallels || {},
|
||||
isDeployed: workflowData.isDeployed || false,
|
||||
deployedAt: workflowData.deployedAt
|
||||
? new Date(workflowData.deployedAt)
|
||||
: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Loaded from database (normalized tables)',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// If no state in DB, use empty state - server should have created start block
|
||||
@@ -494,23 +451,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
deploymentStatuses: {},
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Empty initial state - server should provide start block',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
@@ -604,72 +544,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
folderId: createdWorkflow.folderId,
|
||||
}
|
||||
|
||||
let initialState: any
|
||||
|
||||
// If this is a marketplace import with existing state
|
||||
if (options.marketplaceId && options.marketplaceState) {
|
||||
initialState = {
|
||||
blocks: options.marketplaceState.blocks || {},
|
||||
edges: options.marketplaceState.edges || [],
|
||||
loops: options.marketplaceState.loops || {},
|
||||
parallels: options.marketplaceState.parallels || {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
deploymentStatuses: {}, // Initialize empty deployment statuses map
|
||||
workspaceId, // Include workspace ID in the state object
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: options.marketplaceState.blocks || {},
|
||||
edges: options.marketplaceState.edges || [],
|
||||
loops: options.marketplaceState.loops || {},
|
||||
parallels: options.marketplaceState.parallels || {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
workspaceId, // Include workspace ID in history
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Imported from marketplace',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
logger.info(`Created workflow from marketplace: ${options.marketplaceId}`)
|
||||
} else {
|
||||
// Create empty workflow (no default blocks)
|
||||
initialState = {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
deploymentStatuses: {}, // Initialize empty deployment statuses map
|
||||
workspaceId, // Include workspace ID in the state object
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
workspaceId, // Include workspace ID in history
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Add workflow to registry with server-generated ID
|
||||
@@ -747,23 +623,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
parallels: state.parallels || {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: state.blocks || {},
|
||||
edges: state.edges || [],
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Imported from marketplace',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
@@ -1027,24 +886,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
deployedAt: undefined,
|
||||
workspaceId,
|
||||
deploymentStatuses: {},
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: sourceState.blocks,
|
||||
edges: sourceState.edges,
|
||||
loops: sourceState.loops,
|
||||
parallels: sourceState.parallels,
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
workspaceId,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Duplicated workflow',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
@@ -1156,23 +997,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Workflow deleted',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
})
|
||||
|
||||
|
||||
@@ -253,44 +253,6 @@ describe('workflow store', () => {
|
||||
expect(state.parallels.parallel1).toBeDefined()
|
||||
expect(state.parallels.parallel1.parallelType).toBe('count')
|
||||
})
|
||||
|
||||
it('should save to history when updating parallel properties', () => {
|
||||
const { addBlock, updateParallelCollection, updateParallelCount, updateParallelType } =
|
||||
useWorkflowStore.getState()
|
||||
|
||||
// Add a parallel block
|
||||
addBlock(
|
||||
'parallel1',
|
||||
'parallel',
|
||||
'Test Parallel',
|
||||
{ x: 0, y: 0 },
|
||||
{
|
||||
count: 3,
|
||||
collection: '',
|
||||
}
|
||||
)
|
||||
|
||||
// Get initial history length
|
||||
const initialHistoryLength = useWorkflowStore.getState().history.past.length
|
||||
|
||||
// Update collection
|
||||
updateParallelCollection('parallel1', '["a", "b", "c"]')
|
||||
|
||||
let state = useWorkflowStore.getState()
|
||||
expect(state.history.past.length).toBe(initialHistoryLength + 1)
|
||||
|
||||
// Update count
|
||||
updateParallelCount('parallel1', 5)
|
||||
|
||||
state = useWorkflowStore.getState()
|
||||
expect(state.history.past.length).toBe(initialHistoryLength + 2)
|
||||
|
||||
// Update parallel type
|
||||
updateParallelType('parallel1', 'count')
|
||||
|
||||
state = useWorkflowStore.getState()
|
||||
expect(state.history.past.length).toBe(initialHistoryLength + 3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mode switching', () => {
|
||||
|
||||
@@ -5,11 +5,6 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBlockOutputs } from '@/lib/workflows/block-outputs'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import {
|
||||
pushHistory,
|
||||
type WorkflowStoreWithHistory,
|
||||
withHistory,
|
||||
} from '@/stores/workflows/middleware'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import {
|
||||
@@ -20,8 +15,8 @@ import {
|
||||
import type {
|
||||
Position,
|
||||
SubBlockState,
|
||||
SyncControl,
|
||||
WorkflowState,
|
||||
WorkflowStore,
|
||||
} from '@/stores/workflows/workflow/types'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
|
||||
@@ -39,54 +34,12 @@ const initialState = {
|
||||
// New field for per-workflow deployment tracking
|
||||
deploymentStatuses: {},
|
||||
needsRedeployment: false,
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
isPublished: false,
|
||||
},
|
||||
timestamp: 0,
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
}
|
||||
|
||||
// Create a consolidated sync control implementation
|
||||
/**
|
||||
* Socket-based SyncControl implementation (replaces HTTP sync)
|
||||
*/
|
||||
const createSyncControl = (): SyncControl => ({
|
||||
markDirty: () => {
|
||||
// No-op: Socket-based sync handles this automatically
|
||||
},
|
||||
isDirty: () => {
|
||||
// Always return false since socket sync is real-time
|
||||
return false
|
||||
},
|
||||
forceSync: () => {
|
||||
// No-op: Socket-based sync is always in sync
|
||||
},
|
||||
})
|
||||
|
||||
export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
export const useWorkflowStore = create<WorkflowStore>()(
|
||||
devtools(
|
||||
withHistory((set, get) => ({
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
undo: () => {},
|
||||
redo: () => {},
|
||||
canUndo: () => false,
|
||||
canRedo: () => false,
|
||||
revertToHistoryState: () => {},
|
||||
|
||||
// Implement sync control interface
|
||||
sync: createSyncControl(),
|
||||
|
||||
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => {
|
||||
set({ needsRedeployment })
|
||||
@@ -143,9 +96,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `Add ${type} node`)
|
||||
get().updateLastSaved()
|
||||
// get().sync.markDirty() // Disabled: Using socket-based sync
|
||||
return
|
||||
}
|
||||
|
||||
@@ -199,9 +150,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `Add ${type} block`)
|
||||
get().updateLastSaved()
|
||||
// get().sync.markDirty() // Disabled: Using socket-based sync
|
||||
},
|
||||
|
||||
updateBlockPosition: (id: string, position: Position) => {
|
||||
@@ -310,12 +259,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
})
|
||||
|
||||
set(newState)
|
||||
pushHistory(
|
||||
set,
|
||||
get,
|
||||
newState,
|
||||
parentId ? `Set parent for ${block.name}` : `Remove parent for ${block.name}`
|
||||
)
|
||||
get().updateLastSaved()
|
||||
// Note: Socket.IO handles real-time sync automatically
|
||||
},
|
||||
@@ -386,7 +329,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
})
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, 'Remove block and children')
|
||||
get().updateLastSaved()
|
||||
// Note: Socket.IO handles real-time sync automatically
|
||||
},
|
||||
@@ -426,9 +368,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, 'Add connection')
|
||||
get().updateLastSaved()
|
||||
// get().sync.markDirty() // Disabled: Using socket-based sync
|
||||
},
|
||||
|
||||
removeEdge: (edgeId: string) => {
|
||||
@@ -449,7 +389,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, 'Remove connection')
|
||||
get().updateLastSaved()
|
||||
},
|
||||
|
||||
@@ -459,26 +398,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
state: {
|
||||
blocks: {},
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isDeployed: false,
|
||||
isPublished: false,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
action: 'Initial state',
|
||||
subblockValues: {},
|
||||
},
|
||||
future: [],
|
||||
},
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: false,
|
||||
isPublished: false,
|
||||
}
|
||||
set(newState)
|
||||
// Note: Socket.IO handles real-time sync automatically
|
||||
@@ -585,7 +505,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `Duplicate ${block.type} block`)
|
||||
get().updateLastSaved()
|
||||
// Note: Socket.IO handles real-time sync automatically
|
||||
},
|
||||
@@ -723,7 +642,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `${name} block name updated`)
|
||||
get().updateLastSaved()
|
||||
// Note: Socket.IO handles real-time sync automatically
|
||||
|
||||
@@ -958,7 +876,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
},
|
||||
})
|
||||
|
||||
pushHistory(set, get, newState, 'Reverted to deployed state')
|
||||
get().updateLastSaved()
|
||||
|
||||
// Call API to persist the revert to normalized tables
|
||||
@@ -1042,7 +959,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `Toggle trigger mode for ${block.type} block`)
|
||||
get().updateLastSaved()
|
||||
|
||||
// Handle webhook enable/disable when toggling trigger mode
|
||||
@@ -1111,7 +1027,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `Update parallel count`)
|
||||
get().updateLastSaved()
|
||||
// Note: Socket.IO handles real-time sync automatically
|
||||
},
|
||||
@@ -1139,7 +1054,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `Update parallel collection`)
|
||||
get().updateLastSaved()
|
||||
// Note: Socket.IO handles real-time sync automatically
|
||||
},
|
||||
@@ -1167,7 +1081,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `Update parallel type`)
|
||||
get().updateLastSaved()
|
||||
// Note: Socket.IO handles real-time sync automatically
|
||||
},
|
||||
@@ -1184,7 +1097,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
getDragStartPosition: () => {
|
||||
return get().dragStartPosition || null
|
||||
},
|
||||
})),
|
||||
}),
|
||||
{ name: 'workflow-store' }
|
||||
)
|
||||
)
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface ParallelConfig {
|
||||
parallelType?: 'count' | 'collection'
|
||||
}
|
||||
|
||||
// Generic subflow interface
|
||||
export interface Subflow {
|
||||
id: string
|
||||
workflowId: string
|
||||
@@ -158,16 +157,6 @@ export interface WorkflowState {
|
||||
dragStartPosition?: DragStartPosition | null
|
||||
}
|
||||
|
||||
// New interface for sync control
|
||||
export interface SyncControl {
|
||||
// Mark the workflow as changed, requiring sync
|
||||
markDirty: () => void
|
||||
// Check if the workflow has unsaved changes
|
||||
isDirty: () => boolean
|
||||
// Immediately trigger a sync
|
||||
forceSync: () => void
|
||||
}
|
||||
|
||||
export interface WorkflowActions {
|
||||
addBlock: (
|
||||
id: string,
|
||||
@@ -218,11 +207,6 @@ export interface WorkflowActions {
|
||||
toggleBlockTriggerMode: (id: string) => void
|
||||
setDragStartPosition: (position: DragStartPosition | null) => void
|
||||
getDragStartPosition: () => DragStartPosition | null
|
||||
|
||||
// Add the sync control methods to the WorkflowActions interface
|
||||
sync: SyncControl
|
||||
|
||||
// Add method to get current workflow state (eliminates duplication in diff store)
|
||||
getWorkflowState: () => WorkflowState
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user