diff --git a/apps/sim/app/api/workflows/[id]/variables/route.ts b/apps/sim/app/api/workflows/[id]/variables/route.ts index 7b19a0320a9..1b4cd8ab3b9 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.ts @@ -8,7 +8,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' -import type { Variable } from '@/stores/panel/variables/types' +import type { Variable } from '@/stores/variables/types' const logger = createLogger('WorkflowVariablesAPI') diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts index 7187bf7fc95..41b394b8a77 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { hasWorkflowChanged } from '@/lib/workflows/comparison' import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks' -import { useVariablesStore } from '@/stores/panel/variables/store' +import { useVariablesStore } from '@/stores/variables/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index 68256664a9c..620d38c2e83 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -31,8 +31,8 @@ import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/ import { getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' import { normalizeName } from '@/executor/constants' -import type { Variable } from '@/stores/panel' -import { useVariablesStore } from '@/stores/panel' +import { useVariablesStore } from '@/stores/variables/store' +import type { Variable } from '@/stores/variables/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx index 3cb55879312..c71a48f77bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx @@ -19,8 +19,8 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' -import type { Variable } from '@/stores/panel' -import { useVariablesStore } from '@/stores/panel' +import { useVariablesStore } from '@/stores/variables/store' +import type { Variable } from '@/stores/variables/types' interface VariableAssignment { id: string diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 0f349d1b85f..cc36b7cf9eb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -63,7 +63,8 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useChatStore } from '@/stores/chat/store' import { useNotificationStore } from '@/stores/notifications/store' import type { ChatContext, PanelTab } from '@/stores/panel' -import { usePanelStore, useVariablesStore as usePanelVariablesStore } from '@/stores/panel' +import { usePanelStore } from '@/stores/panel' +import { useVariablesModalStore } from '@/stores/variables/modal' import { useVariablesStore } from '@/stores/variables/store' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils' @@ -205,7 +206,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel setIsChatOpen: state.setIsChatOpen, })) ) - const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore( + const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesModalStore( useShallow((state) => ({ isOpen: state.isOpen, setIsOpen: state.setIsOpen, @@ -482,7 +483,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel throw new Error('No workflow state found') } - const workflowVariables = usePanelVariablesStore + const workflowVariables = useVariablesStore .getState() .getVariablesByWorkflowId(activeWorkflowId) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx index fb43bd5c3d7..a5fb1615c45 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables.tsx @@ -27,16 +27,16 @@ import { usePreventZoom, } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' -import { useVariablesStore as usePanelVariablesStore } from '@/stores/panel' -import type { Variable } from '@/stores/panel/variables/types' import { getVariablesPosition, MAX_VARIABLES_HEIGHT, MAX_VARIABLES_WIDTH, MIN_VARIABLES_HEIGHT, MIN_VARIABLES_WIDTH, - useVariablesStore, -} from '@/stores/variables/store' + useVariablesModalStore, +} from '@/stores/variables/modal' +import { useVariablesStore } from '@/stores/variables/store' +import type { Variable } from '@/stores/variables/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' /** @@ -96,7 +96,7 @@ export function Variables() { const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) const { isOpen, position, width, height, setIsOpen, setPosition, setDimensions } = - useVariablesStore( + useVariablesModalStore( useShallow((s) => ({ isOpen: s.isOpen, position: s.position, @@ -108,7 +108,7 @@ export function Variables() { })) ) - const variables = usePanelVariablesStore((s) => s.variables) + const variables = useVariablesStore((s) => s.variables) const { collaborativeUpdateVariable, collaborativeAddVariable, collaborativeDeleteVariable } = useCollaborativeWorkflow() diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index b7b04f38bb2..bb676157396 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -48,7 +48,7 @@ import { useSkills } from '@/hooks/queries/skills' import { useTablesList } from '@/hooks/queries/tables' import { useWorkflowMap } from '@/hooks/queries/workflows' import { useSelectorDisplayName } from '@/hooks/use-selector-display-name' -import { useVariablesStore } from '@/stores/panel' +import { useVariablesStore } from '@/stores/variables/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { wouldCreateCycle } from '@/stores/workflows/workflow/utils' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index f298bef0248..8b9ca1fa0a9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -36,7 +36,6 @@ import { useExecutionStream } from '@/hooks/use-execution-stream' import { WorkflowValidationError } from '@/serializer' import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution' import { useNotificationStore } from '@/stores/notifications' -import { useVariablesStore } from '@/stores/panel' import { clearExecutionPointer, consolePersistence, @@ -44,6 +43,7 @@ import { saveExecutionPointer, useTerminalConsoleStore, } from '@/stores/terminal' +import { useVariablesStore } from '@/stores/variables/store' import { useWorkflowDiffStore } from '@/stores/workflow-diff' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { mergeSubblockState } from '@/stores/workflows/utils' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 57fd7a41706..a94d4aa9607 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -85,7 +85,7 @@ import { useSearchModalStore } from '@/stores/modals/search/store' import { useNotificationStore } from '@/stores/notifications' import { usePanelEditorStore } from '@/stores/panel' import { useUndoRedoStore } from '@/stores/undo-redo' -import { useVariablesStore } from '@/stores/variables/store' +import { useVariablesModalStore } from '@/stores/variables/modal' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils' @@ -337,7 +337,7 @@ const WorkflowContent = React.memo( autoConnectRef.current = isAutoConnectEnabled // Panel open states for context menu - const isVariablesOpen = useVariablesStore((state) => state.isOpen) + const isVariablesOpen = useVariablesModalStore((state) => state.isOpen) const isChatOpen = useChatStore((state) => state.isChatOpen) const snapGrid: [number, number] = useMemo( @@ -1374,7 +1374,7 @@ const WorkflowContent = React.memo( }, [router, workspaceId, workflowIdParam]) const handleContextToggleVariables = useCallback(() => { - const { isOpen, setIsOpen } = useVariablesStore.getState() + const { isOpen, setIsOpen } = useVariablesModalStore.getState() setIsOpen(!isOpen) }, []) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx index f2c78c680c3..f6c0f9c5b5f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow/components/block/block.tsx @@ -13,7 +13,7 @@ import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { getBlock } from '@/blocks' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' -import { useVariablesStore } from '@/stores/panel/variables/store' +import { useVariablesStore } from '@/stores/variables/store' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' /** Execution status for blocks in preview mode */ diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 0b22a47de4b..b878b89a274 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -19,8 +19,9 @@ import { } from '@/socket/constants' import { useNotificationStore } from '@/stores/notifications' import { registerEmitFunctions, useOperationQueue } from '@/stores/operation-queue/store' -import { usePanelEditorStore, useVariablesStore } from '@/stores/panel' +import { usePanelEditorStore } from '@/stores/panel' import { useCodeUndoRedoStore, useUndoRedoStore } from '@/stores/undo-redo' +import { useVariablesStore } from '@/stores/variables/store' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' diff --git a/apps/sim/lib/workflows/persistence/duplicate.ts b/apps/sim/lib/workflows/persistence/duplicate.ts index 1895068ab4d..bbc07a5ad15 100644 --- a/apps/sim/lib/workflows/persistence/duplicate.ts +++ b/apps/sim/lib/workflows/persistence/duplicate.ts @@ -14,7 +14,7 @@ import { deduplicateWorkflowName, } from '@/lib/workflows/utils' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' -import type { Variable } from '@/stores/panel/variables/types' +import type { Variable } from '@/stores/variables/types' import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowDuplicateHelper') diff --git a/apps/sim/lib/workflows/variables/variable-manager.ts b/apps/sim/lib/workflows/variables/variable-manager.ts index 7807d466c0e..61e5dfaee39 100644 --- a/apps/sim/lib/workflows/variables/variable-manager.ts +++ b/apps/sim/lib/workflows/variables/variable-manager.ts @@ -1,4 +1,4 @@ -import type { VariableType } from '@/stores/panel/variables/types' +import type { VariableType } from '@/stores/variables/types' /** * Central manager for handling all variable-related operations. diff --git a/apps/sim/stores/panel/index.ts b/apps/sim/stores/panel/index.ts index 13ec5f1eec3..cf2c541411f 100644 --- a/apps/sim/stores/panel/index.ts +++ b/apps/sim/stores/panel/index.ts @@ -9,6 +9,3 @@ export { usePanelStore } from './store' // Toolbar export { useToolbarStore } from './toolbar' export type { ChatContext, PanelState, PanelTab } from './types' -export type { Variable, VariablesStore, VariableType } from './variables' -// Variables -export { useVariablesStore } from './variables' diff --git a/apps/sim/stores/panel/variables/index.ts b/apps/sim/stores/panel/variables/index.ts deleted file mode 100644 index 6a34a434565..00000000000 --- a/apps/sim/stores/panel/variables/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useVariablesStore } from './store' -export type { Variable, VariablesStore, VariableType } from './types' diff --git a/apps/sim/stores/panel/variables/store.ts b/apps/sim/stores/panel/variables/store.ts deleted file mode 100644 index dbd8145ba55..00000000000 --- a/apps/sim/stores/panel/variables/store.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { createLogger } from '@sim/logger' -import JSON5 from 'json5' -import { create } from 'zustand' -import { devtools } from 'zustand/middleware' -import { normalizeName } from '@/executor/constants' -import { useOperationQueueStore } from '@/stores/operation-queue/store' -import type { Variable, VariablesStore } from '@/stores/panel/variables/types' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' - -const logger = createLogger('VariablesStore') - -function validateVariable(variable: Variable): string | undefined { - try { - switch (variable.type) { - case 'number': - if (Number.isNaN(Number(variable.value))) { - return 'Not a valid number' - } - break - case 'boolean': - if (!/^(true|false)$/i.test(String(variable.value).trim())) { - return 'Expected "true" or "false"' - } - break - case 'object': - try { - const valueToEvaluate = String(variable.value).trim() - - if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) { - return 'Not a valid object format' - } - - const parsed = JSON5.parse(valueToEvaluate) - - if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { - return 'Not a valid object' - } - - return undefined - } catch (e) { - logger.error('Object parsing error:', e) - return 'Invalid object syntax' - } - case 'array': - try { - const parsed = JSON5.parse(String(variable.value)) - if (!Array.isArray(parsed)) { - return 'Not a valid array' - } - } catch { - return 'Invalid array syntax' - } - break - } - return undefined - } catch (e) { - return e instanceof Error ? e.message : 'Invalid format' - } -} - -function migrateStringToPlain(variable: Variable): Variable { - if (variable.type !== 'string') { - return variable - } - - const updated = { - ...variable, - type: 'plain' as const, - } - - return updated -} - -export const useVariablesStore = create()( - devtools((set, get) => ({ - variables: {}, - isLoading: false, - error: null, - isEditing: null, - - addVariable: (variable, providedId?: string) => { - const id = providedId || crypto.randomUUID() - - const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId) - - if (!variable.name || /^variable\d+$/.test(variable.name)) { - const existingNumbers = workflowVariables - .map((v) => { - const match = v.name.match(/^variable(\d+)$/) - return match ? Number.parseInt(match[1]) : 0 - }) - .filter((n) => !Number.isNaN(n)) - - const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1 - - variable.name = `variable${nextNumber}` - } - - let uniqueName = variable.name - let nameIndex = 1 - - while (workflowVariables.some((v) => v.name === uniqueName)) { - uniqueName = `${variable.name} (${nameIndex})` - nameIndex++ - } - - if (variable.type === 'string') { - variable.type = 'plain' - } - - const newVariable: Variable = { - id, - workflowId: variable.workflowId, - name: uniqueName, - type: variable.type, - value: variable.value || '', - validationError: undefined, - } - - const validationError = validateVariable(newVariable) - if (validationError) { - newVariable.validationError = validationError - } - - set((state) => ({ - variables: { - ...state.variables, - [id]: newVariable, - }, - })) - - return id - }, - - updateVariable: (id, update) => { - set((state) => { - if (!state.variables[id]) return state - - if (update.name !== undefined) { - const oldVariable = state.variables[id] - const oldVariableName = oldVariable.name - const newName = update.name.trim() - - if (!newName) { - update = { ...update } - update.name = undefined - } else if (newName !== oldVariableName) { - const subBlockStore = useSubBlockStore.getState() - const targetWorkflowId = oldVariable.workflowId - - if (targetWorkflowId) { - const workflowValues = subBlockStore.workflowValues[targetWorkflowId] || {} - const updatedWorkflowValues = { ...workflowValues } - const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> = - [] - - const oldVarName = normalizeName(oldVariableName) - const newVarName = normalizeName(newName) - const regex = new RegExp(``, 'gi') - - const updateReferences = (value: any, pattern: RegExp, replacement: string): any => { - if (typeof value === 'string') { - return pattern.test(value) ? value.replace(pattern, replacement) : value - } - - if (Array.isArray(value)) { - return value.map((item) => updateReferences(item, pattern, replacement)) - } - - if (value !== null && typeof value === 'object') { - const result = { ...value } - for (const key in result) { - result[key] = updateReferences(result[key], pattern, replacement) - } - return result - } - - return value - } - - Object.entries(workflowValues).forEach(([blockId, blockValues]) => { - Object.entries(blockValues as Record).forEach( - ([subBlockId, value]) => { - const updatedValue = updateReferences(value, regex, ``) - - if (JSON.stringify(updatedValue) !== JSON.stringify(value)) { - if (!updatedWorkflowValues[blockId]) { - updatedWorkflowValues[blockId] = { ...workflowValues[blockId] } - } - updatedWorkflowValues[blockId][subBlockId] = updatedValue - changedSubBlocks.push({ blockId, subBlockId, value: updatedValue }) - } - } - ) - }) - - // Update local state - useSubBlockStore.setState({ - workflowValues: { - ...subBlockStore.workflowValues, - [targetWorkflowId]: updatedWorkflowValues, - }, - }) - - // Queue operations for persistence via socket - const operationQueue = useOperationQueueStore.getState() - - for (const { blockId, subBlockId, value } of changedSubBlocks) { - operationQueue.addToQueue({ - id: crypto.randomUUID(), - operation: { - operation: 'subblock-update', - target: 'subblock', - payload: { blockId, subblockId: subBlockId, value }, - }, - workflowId: targetWorkflowId, - userId: 'system', - }) - } - } - } - } - - if (update.type === 'string') { - update = { ...update, type: 'plain' } - } - - const updatedVariable: Variable = { - ...state.variables[id], - ...update, - validationError: undefined, - } - - if (update.type || update.value !== undefined) { - updatedVariable.validationError = validateVariable(updatedVariable) - } - - const updated = { - ...state.variables, - [id]: updatedVariable, - } - - return { variables: updated } - }) - }, - - deleteVariable: (id) => { - set((state) => { - if (!state.variables[id]) return state - - const workflowId = state.variables[id].workflowId - const { [id]: _, ...rest } = state.variables - - return { variables: rest } - }) - }, - - getVariablesByWorkflowId: (workflowId) => { - return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId) - }, - })) -) diff --git a/apps/sim/stores/panel/variables/types.ts b/apps/sim/stores/panel/variables/types.ts deleted file mode 100644 index 7cdfb60d430..00000000000 --- a/apps/sim/stores/panel/variables/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Variable types supported in the application - * Note: 'string' is deprecated - use 'plain' for text values instead - */ -export type VariableType = 'plain' | 'number' | 'boolean' | 'object' | 'array' | 'string' - -/** - * Represents a workflow variable with workflow-specific naming - * Variable names must be unique within each workflow - */ -export interface Variable { - id: string - workflowId: string - name: string // Must be unique per workflow - type: VariableType - value: unknown - validationError?: string // Tracks format validation errors -} - -export interface VariablesStore { - variables: Record - isLoading: boolean - error: string | null - isEditing: string | null - - /** - * Adds a new variable with automatic name uniqueness validation - * If a variable with the same name exists, it will be suffixed with a number - * Optionally accepts a predetermined ID for collaborative operations - */ - addVariable: (variable: Omit, providedId?: string) => string - - /** - * Updates a variable, ensuring name remains unique within the workflow - * If an updated name conflicts with existing ones, a numbered suffix is added - */ - updateVariable: (id: string, update: Partial>) => void - - deleteVariable: (id: string) => void - - /** - * Returns all variables for a specific workflow - */ - getVariablesByWorkflowId: (workflowId: string) => Variable[] -} diff --git a/apps/sim/stores/variables/index.ts b/apps/sim/stores/variables/index.ts new file mode 100644 index 00000000000..d64ccb216a5 --- /dev/null +++ b/apps/sim/stores/variables/index.ts @@ -0,0 +1,11 @@ +export { + getDefaultVariablesDimensions, + getVariablesPosition, + MAX_VARIABLES_HEIGHT, + MAX_VARIABLES_WIDTH, + MIN_VARIABLES_HEIGHT, + MIN_VARIABLES_WIDTH, + useVariablesModalStore, +} from './modal' +export { useVariablesStore } from './store' +export type { Variable, VariablesStore, VariableType } from './types' diff --git a/apps/sim/stores/variables/modal.ts b/apps/sim/stores/variables/modal.ts new file mode 100644 index 00000000000..be8ff2915b7 --- /dev/null +++ b/apps/sim/stores/variables/modal.ts @@ -0,0 +1,145 @@ +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' +import type { + VariablesDimensions, + VariablesModalStore, + VariablesPosition, +} from '@/stores/variables/types' + +/** + * Floating variables modal default dimensions. + * Slightly larger than the chat modal for more comfortable editing. + */ +const DEFAULT_WIDTH = 320 +const DEFAULT_HEIGHT = 320 + +/** + * Minimum and maximum modal dimensions. + */ +export const MIN_VARIABLES_WIDTH = DEFAULT_WIDTH +export const MIN_VARIABLES_HEIGHT = DEFAULT_HEIGHT +export const MAX_VARIABLES_WIDTH = 500 +export const MAX_VARIABLES_HEIGHT = 600 + +/** Inset gap between the viewport edge and the content window */ +const CONTENT_WINDOW_GAP = 8 + +/** + * Compute a center-biased default position, factoring in current layout chrome + * (sidebar, right panel, terminal) and content window inset. + */ +const calculateDefaultPosition = (): VariablesPosition => { + if (typeof window === 'undefined') { + return { x: 100, y: 100 } + } + + const sidebarWidth = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0' + ) + const panelWidth = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0' + ) + const terminalHeight = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0' + ) + + const availableWidth = window.innerWidth - sidebarWidth - CONTENT_WINDOW_GAP - panelWidth + const availableHeight = window.innerHeight - CONTENT_WINDOW_GAP * 2 - terminalHeight + const x = sidebarWidth + (availableWidth - DEFAULT_WIDTH) / 2 + const y = CONTENT_WINDOW_GAP + (availableHeight - DEFAULT_HEIGHT) / 2 + return { x, y } +} + +/** + * Constrain a position to the visible canvas, considering layout chrome. + */ +const constrainPosition = ( + position: VariablesPosition, + width: number = DEFAULT_WIDTH, + height: number = DEFAULT_HEIGHT +): VariablesPosition => { + if (typeof window === 'undefined') return position + + const sidebarWidth = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0' + ) + const panelWidth = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0' + ) + const terminalHeight = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0' + ) + + const minX = sidebarWidth + const maxX = window.innerWidth - CONTENT_WINDOW_GAP - panelWidth - width + const minY = CONTENT_WINDOW_GAP + const maxY = window.innerHeight - CONTENT_WINDOW_GAP - terminalHeight - height + + return { + x: Math.max(minX, Math.min(maxX, position.x)), + y: Math.max(minY, Math.min(maxY, position.y)), + } +} + +/** + * Return a valid, constrained position. If the stored one is off-bounds due to + * layout changes, prefer a fresh default center position. + */ +export const getVariablesPosition = ( + stored: VariablesPosition | null, + width: number = DEFAULT_WIDTH, + height: number = DEFAULT_HEIGHT +): VariablesPosition => { + if (!stored) return calculateDefaultPosition() + const constrained = constrainPosition(stored, width, height) + const deltaX = Math.abs(constrained.x - stored.x) + const deltaY = Math.abs(constrained.y - stored.y) + if (deltaX > 100 || deltaY > 100) return calculateDefaultPosition() + return constrained +} + +/** + * UI-only store for the floating variables modal. + * Variable data lives in the variables data store (`@/stores/variables/store`). + */ +export const useVariablesModalStore = create()( + devtools( + persist( + (set) => ({ + isOpen: false, + position: null, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + + setIsOpen: (open) => set({ isOpen: open }), + setPosition: (position) => set({ position }), + setDimensions: (dimensions) => + set({ + width: Math.max(MIN_VARIABLES_WIDTH, Math.min(MAX_VARIABLES_WIDTH, dimensions.width)), + height: Math.max( + MIN_VARIABLES_HEIGHT, + Math.min(MAX_VARIABLES_HEIGHT, dimensions.height) + ), + }), + resetPosition: () => set({ position: null }), + }), + { + name: 'variables-modal-store', + partialize: (state) => ({ + position: state.position, + width: state.width, + height: state.height, + }), + } + ), + { name: 'variables-modal-store' } + ) +) + +/** + * Get default floating variables modal dimensions. + */ +export const getDefaultVariablesDimensions = (): VariablesDimensions => ({ + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, +}) diff --git a/apps/sim/stores/variables/store.ts b/apps/sim/stores/variables/store.ts index 47a9039ae18..0980f2ae6b3 100644 --- a/apps/sim/stores/variables/store.ts +++ b/apps/sim/stores/variables/store.ts @@ -1,145 +1,248 @@ +import { createLogger } from '@sim/logger' +import JSON5 from 'json5' import { create } from 'zustand' -import { devtools, persist } from 'zustand/middleware' -import type { - VariablesDimensions, - VariablesModalStore, - VariablesPosition, -} from '@/stores/variables/types' - -/** - * Floating variables modal default dimensions. - * Slightly larger than the chat modal for more comfortable editing. - */ -const DEFAULT_WIDTH = 320 -const DEFAULT_HEIGHT = 320 - -/** - * Minimum and maximum modal dimensions. - */ -export const MIN_VARIABLES_WIDTH = DEFAULT_WIDTH -export const MIN_VARIABLES_HEIGHT = DEFAULT_HEIGHT -export const MAX_VARIABLES_WIDTH = 500 -export const MAX_VARIABLES_HEIGHT = 600 - -/** Inset gap between the viewport edge and the content window */ -const CONTENT_WINDOW_GAP = 8 - -/** - * Compute a center-biased default position, factoring in current layout chrome - * (sidebar, right panel, terminal) and content window inset. - */ -const calculateDefaultPosition = (): VariablesPosition => { - if (typeof window === 'undefined') { - return { x: 100, y: 100 } - } +import { devtools } from 'zustand/middleware' +import { normalizeName } from '@/executor/constants' +import { useOperationQueueStore } from '@/stores/operation-queue/store' +import type { Variable, VariablesStore } from '@/stores/variables/types' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' - const sidebarWidth = Number.parseInt( - getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0' - ) - const panelWidth = Number.parseInt( - getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0' - ) - const terminalHeight = Number.parseInt( - getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0' - ) - - const availableWidth = window.innerWidth - sidebarWidth - CONTENT_WINDOW_GAP - panelWidth - const availableHeight = window.innerHeight - CONTENT_WINDOW_GAP * 2 - terminalHeight - const x = sidebarWidth + (availableWidth - DEFAULT_WIDTH) / 2 - const y = CONTENT_WINDOW_GAP + (availableHeight - DEFAULT_HEIGHT) / 2 - return { x, y } -} +const logger = createLogger('VariablesStore') + +function validateVariable(variable: Variable): string | undefined { + try { + switch (variable.type) { + case 'number': + if (Number.isNaN(Number(variable.value))) { + return 'Not a valid number' + } + break + case 'boolean': + if (!/^(true|false)$/i.test(String(variable.value).trim())) { + return 'Expected "true" or "false"' + } + break + case 'object': + try { + const valueToEvaluate = String(variable.value).trim() + + if (!valueToEvaluate.startsWith('{') || !valueToEvaluate.endsWith('}')) { + return 'Not a valid object format' + } + + const parsed = JSON5.parse(valueToEvaluate) + + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + return 'Not a valid object' + } -/** - * Constrain a position to the visible canvas, considering layout chrome. - */ -const constrainPosition = ( - position: VariablesPosition, - width: number = DEFAULT_WIDTH, - height: number = DEFAULT_HEIGHT -): VariablesPosition => { - if (typeof window === 'undefined') return position - - const sidebarWidth = Number.parseInt( - getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0' - ) - const panelWidth = Number.parseInt( - getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0' - ) - const terminalHeight = Number.parseInt( - getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0' - ) - - const minX = sidebarWidth - const maxX = window.innerWidth - CONTENT_WINDOW_GAP - panelWidth - width - const minY = CONTENT_WINDOW_GAP - const maxY = window.innerHeight - CONTENT_WINDOW_GAP - terminalHeight - height - - return { - x: Math.max(minX, Math.min(maxX, position.x)), - y: Math.max(minY, Math.min(maxY, position.y)), + return undefined + } catch (e) { + logger.error('Object parsing error:', e) + return 'Invalid object syntax' + } + case 'array': + try { + const parsed = JSON5.parse(String(variable.value)) + if (!Array.isArray(parsed)) { + return 'Not a valid array' + } + } catch { + return 'Invalid array syntax' + } + break + } + return undefined + } catch (e) { + return e instanceof Error ? e.message : 'Invalid format' } } -/** - * Return a valid, constrained position. If the stored one is off-bounds due to - * layout changes, prefer a fresh default center position. - */ -export const getVariablesPosition = ( - stored: VariablesPosition | null, - width: number = DEFAULT_WIDTH, - height: number = DEFAULT_HEIGHT -): VariablesPosition => { - if (!stored) return calculateDefaultPosition() - const constrained = constrainPosition(stored, width, height) - const deltaX = Math.abs(constrained.x - stored.x) - const deltaY = Math.abs(constrained.y - stored.y) - if (deltaX > 100 || deltaY > 100) return calculateDefaultPosition() - return constrained -} +export const useVariablesStore = create()( + devtools((set, get) => ({ + variables: {}, + isLoading: false, + error: null, + isEditing: null, + + addVariable: (variable, providedId?: string) => { + const id = providedId || crypto.randomUUID() + + const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId) -/** - * UI-only store for the floating variables modal. - * Variable data lives in the panel variables store (`@/stores/panel/variables`). - */ -export const useVariablesStore = create()( - devtools( - persist( - (set) => ({ - isOpen: false, - position: null, - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - - setIsOpen: (open) => set({ isOpen: open }), - setPosition: (position) => set({ position }), - setDimensions: (dimensions) => - set({ - width: Math.max(MIN_VARIABLES_WIDTH, Math.min(MAX_VARIABLES_WIDTH, dimensions.width)), - height: Math.max( - MIN_VARIABLES_HEIGHT, - Math.min(MAX_VARIABLES_HEIGHT, dimensions.height) - ), - }), - resetPosition: () => set({ position: null }), - }), - { - name: 'variables-modal-store', - partialize: (state) => ({ - position: state.position, - width: state.width, - height: state.height, - }), + if (!variable.name || /^variable\d+$/.test(variable.name)) { + const existingNumbers = workflowVariables + .map((v) => { + const match = v.name.match(/^variable(\d+)$/) + return match ? Number.parseInt(match[1]) : 0 + }) + .filter((n) => !Number.isNaN(n)) + + const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1 + + variable.name = `variable${nextNumber}` } - ), - { name: 'variables-modal-store' } - ) -) -/** - * Get default floating variables modal dimensions. - */ -export const getDefaultVariablesDimensions = (): VariablesDimensions => ({ - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, -}) + let uniqueName = variable.name + let nameIndex = 1 + + while (workflowVariables.some((v) => v.name === uniqueName)) { + uniqueName = `${variable.name} (${nameIndex})` + nameIndex++ + } + + if (variable.type === 'string') { + variable.type = 'plain' + } + + const newVariable: Variable = { + id, + workflowId: variable.workflowId, + name: uniqueName, + type: variable.type, + value: variable.value || '', + validationError: undefined, + } + + const validationError = validateVariable(newVariable) + if (validationError) { + newVariable.validationError = validationError + } + + set((state) => ({ + variables: { + ...state.variables, + [id]: newVariable, + }, + })) + + return id + }, + + updateVariable: (id, update) => { + set((state) => { + if (!state.variables[id]) return state + + if (update.name !== undefined) { + const oldVariable = state.variables[id] + const oldVariableName = oldVariable.name + const newName = update.name.trim() + + if (!newName) { + update = { ...update } + update.name = undefined + } else if (newName !== oldVariableName) { + const subBlockStore = useSubBlockStore.getState() + const targetWorkflowId = oldVariable.workflowId + + if (targetWorkflowId) { + const workflowValues = subBlockStore.workflowValues[targetWorkflowId] || {} + const updatedWorkflowValues = { ...workflowValues } + const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> = + [] + + const oldVarName = normalizeName(oldVariableName) + const newVarName = normalizeName(newName) + const regex = new RegExp(``, 'gi') + + const updateReferences = (value: any, pattern: RegExp, replacement: string): any => { + if (typeof value === 'string') { + return pattern.test(value) ? value.replace(pattern, replacement) : value + } + + if (Array.isArray(value)) { + return value.map((item) => updateReferences(item, pattern, replacement)) + } + + if (value !== null && typeof value === 'object') { + const result = { ...value } + for (const key in result) { + result[key] = updateReferences(result[key], pattern, replacement) + } + return result + } + + return value + } + + Object.entries(workflowValues).forEach(([blockId, blockValues]) => { + Object.entries(blockValues as Record).forEach( + ([subBlockId, value]) => { + const updatedValue = updateReferences(value, regex, ``) + + if (JSON.stringify(updatedValue) !== JSON.stringify(value)) { + if (!updatedWorkflowValues[blockId]) { + updatedWorkflowValues[blockId] = { ...workflowValues[blockId] } + } + updatedWorkflowValues[blockId][subBlockId] = updatedValue + changedSubBlocks.push({ blockId, subBlockId, value: updatedValue }) + } + } + ) + }) + + // Update local state + useSubBlockStore.setState({ + workflowValues: { + ...subBlockStore.workflowValues, + [targetWorkflowId]: updatedWorkflowValues, + }, + }) + + // Queue operations for persistence via socket + const operationQueue = useOperationQueueStore.getState() + + for (const { blockId, subBlockId, value } of changedSubBlocks) { + operationQueue.addToQueue({ + id: crypto.randomUUID(), + operation: { + operation: 'subblock-update', + target: 'subblock', + payload: { blockId, subblockId: subBlockId, value }, + }, + workflowId: targetWorkflowId, + userId: 'system', + }) + } + } + } + } + + if (update.type === 'string') { + update = { ...update, type: 'plain' } + } + + const updatedVariable: Variable = { + ...state.variables[id], + ...update, + validationError: undefined, + } + + if (update.type || update.value !== undefined) { + updatedVariable.validationError = validateVariable(updatedVariable) + } + + const updated = { + ...state.variables, + [id]: updatedVariable, + } + + return { variables: updated } + }) + }, + + deleteVariable: (id) => { + set((state) => { + if (!state.variables[id]) return state + + const { [id]: _, ...rest } = state.variables + + return { variables: rest } + }) + }, + + getVariablesByWorkflowId: (workflowId) => { + return Object.values(get().variables).filter((variable) => variable.workflowId === workflowId) + }, + })) +) diff --git a/apps/sim/stores/variables/types.ts b/apps/sim/stores/variables/types.ts index 89ed0fd62fb..a3b43e5be8b 100644 --- a/apps/sim/stores/variables/types.ts +++ b/apps/sim/stores/variables/types.ts @@ -1,3 +1,49 @@ +/** + * Variable types supported in the application + * Note: 'string' is deprecated - use 'plain' for text values instead + */ +export type VariableType = 'plain' | 'number' | 'boolean' | 'object' | 'array' | 'string' + +/** + * Represents a workflow variable with workflow-specific naming + * Variable names must be unique within each workflow + */ +export interface Variable { + id: string + workflowId: string + name: string // Must be unique per workflow + type: VariableType + value: unknown + validationError?: string // Tracks format validation errors +} + +export interface VariablesStore { + variables: Record + isLoading: boolean + error: string | null + isEditing: string | null + + /** + * Adds a new variable with automatic name uniqueness validation + * If a variable with the same name exists, it will be suffixed with a number + * Optionally accepts a predetermined ID for collaborative operations + */ + addVariable: (variable: Omit, providedId?: string) => string + + /** + * Updates a variable, ensuring name remains unique within the workflow + * If an updated name conflicts with existing ones, a numbered suffix is added + */ + updateVariable: (id: string, update: Partial>) => void + + deleteVariable: (id: string) => void + + /** + * Returns all variables for a specific workflow + */ + getVariablesByWorkflowId: (workflowId: string) => Variable[] +} + /** * 2D position used by the floating variables modal. */ @@ -16,7 +62,7 @@ export interface VariablesDimensions { /** * UI-only store interface for the floating variables modal. - * Variable data lives in the panel variables store (`@/stores/panel/variables`). + * Variable data lives in the variables data store (`@/stores/variables/store`). */ export interface VariablesModalStore { isOpen: boolean diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index 59be5bf8b3d..48a5b4a7a7c 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -6,8 +6,8 @@ import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments' import { deploymentKeys } from '@/hooks/queries/deployments' import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists' -import { useVariablesStore } from '@/stores/panel/variables/store' -import type { Variable } from '@/stores/panel/variables/types' +import { useVariablesStore } from '@/stores/variables/store' +import type { Variable } from '@/stores/variables/types' import type { HydrationState, WorkflowRegistry } from '@/stores/workflows/registry/types' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { getUniqueBlockName, regenerateBlockIds } from '@/stores/workflows/utils'