diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts index 3986164f0fd..555496c899a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-selector-setup.ts @@ -5,8 +5,8 @@ import { useParams } from 'next/navigation' import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context' import type { SubBlockConfig } from '@/blocks/types' import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants' +import { usePersonalEnvironment } from '@/hooks/queries/environment' import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' -import { useEnvironmentStore } from '@/stores/settings/environment' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useDependsOnGate } from './use-depends-on-gate' import { useSubBlockValue } from './use-sub-block-value' @@ -32,7 +32,7 @@ export function useSelectorSetup( const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) const workflowId = (params?.workflowId as string) || activeWorkflowId || '' - const envVariables = useEnvironmentStore((s) => s.variables) + const { data: envVariables = {} } = usePersonalEnvironment() const { finalDisabled, dependencyValues, canonicalIndex } = useDependsOnGate( blockId, 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 7c182e285c6..fb43bd5c3d7 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 @@ -28,6 +28,7 @@ import { } 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, @@ -36,7 +37,6 @@ import { MIN_VARIABLES_WIDTH, useVariablesStore, } from '@/stores/variables/store' -import type { Variable } from '@/stores/variables/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' /** 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 2e8e4b8ce4a..f298bef0248 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 @@ -37,7 +37,6 @@ import { WorkflowValidationError } from '@/serializer' import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution' import { useNotificationStore } from '@/stores/notifications' import { useVariablesStore } from '@/stores/panel' -import { useEnvironmentStore } from '@/stores/settings/environment' import { clearExecutionPointer, consolePersistence, @@ -120,7 +119,6 @@ export function useWorkflowExecution() { })) ) const hasHydrated = useTerminalConsoleStore((s) => s._hasHydrated) - const getAllVariables = useEnvironmentStore((s) => s.getAllVariables) const { getVariablesByWorkflowId, variables } = useVariablesStore( useShallow((s) => ({ getVariablesByWorkflowId: s.getVariablesByWorkflowId, @@ -744,7 +742,6 @@ export function useWorkflowExecution() { activeWorkflowId, currentWorkflow, toggleConsole, - getAllVariables, getVariablesByWorkflowId, setIsExecuting, setIsDebugging, diff --git a/apps/sim/hooks/queries/environment.ts b/apps/sim/hooks/queries/environment.ts index 01f6f6b9929..6d9c2dab94a 100644 --- a/apps/sim/hooks/queries/environment.ts +++ b/apps/sim/hooks/queries/environment.ts @@ -1,4 +1,3 @@ -import { useEffect } from 'react' import { createLogger } from '@sim/logger' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { WorkspaceEnvironmentData } from '@/lib/environment/api' @@ -6,7 +5,6 @@ import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/envir import { workspaceCredentialKeys } from '@/hooks/queries/credentials' import { API_ENDPOINTS } from '@/stores/constants' import type { EnvironmentVariable } from '@/stores/settings/environment' -import { useEnvironmentStore } from '@/stores/settings/environment' export type { WorkspaceEnvironmentData } from '@/lib/environment/api' export type { EnvironmentVariable } from '@/stores/settings/environment' @@ -22,29 +20,16 @@ export const environmentKeys = { workspace: (workspaceId: string) => [...environmentKeys.all, 'workspace', workspaceId] as const, } -/** - * Environment Variable Types - */ /** * Hook to fetch personal environment variables */ export function usePersonalEnvironment() { - const setVariables = useEnvironmentStore((state) => state.setVariables) - - const query = useQuery({ + return useQuery({ queryKey: environmentKeys.personal(), queryFn: ({ signal }) => fetchPersonalEnvironment(signal), staleTime: 60 * 1000, // 1 minute placeholderData: keepPreviousData, }) - - useEffect(() => { - if (query.data) { - setVariables(query.data) - } - }, [query.data, setVariables]) - - return query } /** diff --git a/apps/sim/hooks/selectors/use-selector-query.ts b/apps/sim/hooks/selectors/use-selector-query.ts index 0323c7f5e7d..a4444b762aa 100644 --- a/apps/sim/hooks/selectors/use-selector-query.ts +++ b/apps/sim/hooks/selectors/use-selector-query.ts @@ -1,9 +1,9 @@ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants' +import { usePersonalEnvironment } from '@/hooks/queries/environment' import { getSelectorDefinition, mergeOption } from '@/hooks/selectors/registry' import type { SelectorKey, SelectorOption, SelectorQueryArgs } from '@/hooks/selectors/types' -import { useEnvironmentStore } from '@/stores/settings/environment' interface SelectorHookArgs extends Omit { search?: string @@ -31,7 +31,7 @@ export function useSelectorOptionDetail( key: SelectorKey, args: SelectorHookArgs & { detailId?: string } ) { - const envVariables = useEnvironmentStore((s) => s.variables) + const { data: envVariables = {} } = usePersonalEnvironment() const definition = getSelectorDefinition(key) const resolvedDetailId = useMemo(() => { diff --git a/apps/sim/stores/index.ts b/apps/sim/stores/index.ts index d1bbbc9b227..acc0d5f2f7d 100644 --- a/apps/sim/stores/index.ts +++ b/apps/sim/stores/index.ts @@ -1,10 +1,9 @@ 'use client' -import { useEffect } from 'react' import { createLogger } from '@sim/logger' +import { getQueryClient } from '@/app/_shell/providers/get-query-client' +import { environmentKeys } from '@/hooks/queries/environment' import { useExecutionStore } from '@/stores/execution' -import { useVariablesStore } from '@/stores/panel' -import { useEnvironmentStore } from '@/stores/settings/environment' import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -12,194 +11,10 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('Stores') -// Track initialization state -let isInitializing = false -let appFullyInitialized = false -let dataInitialized = false // Flag for actual data loading completion - -/** - * Initialize the application state and sync system - * localStorage persistence has been removed - relies on DB and Zustand stores only - */ -async function initializeApplication(): Promise { - if (typeof window === 'undefined' || isInitializing) return - - isInitializing = true - appFullyInitialized = false - - // Track initialization start time - const initStartTime = Date.now() - - try { - // Load environment variables directly from DB - await useEnvironmentStore.getState().loadEnvironmentVariables() - - // Mark data as initialized only after sync managers have loaded data from DB - dataInitialized = true - - // Log initialization timing information - const initDuration = Date.now() - initStartTime - logger.info(`Application initialization completed in ${initDuration}ms`) - - // Mark application as fully initialized - appFullyInitialized = true - } catch (error) { - logger.error('Error during application initialization:', { error }) - // Still mark as initialized to prevent being stuck in initializing state - appFullyInitialized = true - // But don't mark data as initialized on error - dataInitialized = false - } finally { - isInitializing = false - } -} - -/** - * Checks if application is fully initialized - */ -export function isAppInitialized(): boolean { - return appFullyInitialized -} - -/** - * Checks if data has been loaded from the database - * This should be checked before any sync operations - */ -export function isDataInitialized(): boolean { - return dataInitialized -} - -/** - * Handle application cleanup before unload - */ -function handleBeforeUnload(event: BeforeUnloadEvent): void { - // Check if we're on an authentication page and skip confirmation if we are - if (typeof window !== 'undefined') { - const path = window.location.pathname - // Skip confirmation for auth-related pages - if ( - path === '/login' || - path === '/signup' || - path === '/reset-password' || - path === '/verify' - ) { - return - } - } - - // Standard beforeunload pattern - event.preventDefault() - event.returnValue = '' -} - -/** - * Clean up sync system - */ -function cleanupApplication(): void { - window.removeEventListener('beforeunload', handleBeforeUnload) - // Note: No sync managers to dispose - Socket.IO handles cleanup -} - -/** - * Clear all user data when signing out - * localStorage persistence has been removed - */ -export async function clearUserData(): Promise { - if (typeof window === 'undefined') return - - try { - // Note: No sync managers to dispose - Socket.IO handles cleanup - - // Reset all stores to their initial state - resetAllStores() - - // Clear localStorage except for essential app settings (minimal usage) - const keysToKeep = ['next-favicon', 'theme'] - const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key)) - keysToRemove.forEach((key) => localStorage.removeItem(key)) - - // Reset application initialization state - appFullyInitialized = false - dataInitialized = false - - logger.info('User data cleared successfully') - } catch (error) { - logger.error('Error clearing user data:', { error }) - } -} - -/** - * Hook to manage application lifecycle - */ -export function useAppInitialization() { - useEffect(() => { - // Use Promise to handle async initialization - initializeApplication() - - return () => { - cleanupApplication() - } - }, []) -} - -/** - * Hook to reinitialize the application after successful login - * Use this in the login success handler or post-login page - */ -export function useLoginInitialization() { - useEffect(() => { - reinitializeAfterLogin() - }, []) -} - /** - * Reinitialize the application after login - * This ensures we load fresh data from the database for the new user + * Reset all Zustand stores and React Query caches to initial state. */ -export async function reinitializeAfterLogin(): Promise { - if (typeof window === 'undefined') return - - try { - // Reset application initialization state - appFullyInitialized = false - dataInitialized = false - - // Note: No sync managers to dispose - Socket.IO handles cleanup - - // Clean existing state to avoid stale data - resetAllStores() - - // Reset initialization flags to force a fresh load - isInitializing = false - - // Reinitialize the application - await initializeApplication() - - logger.info('Application reinitialized after login') - } catch (error) { - logger.error('Error reinitializing application:', { error }) - } -} - -// Initialize immediately when imported on client -if (typeof window !== 'undefined') { - initializeApplication() -} - -// Export all stores -export { - useWorkflowStore, - useWorkflowRegistry, - useEnvironmentStore, - useExecutionStore, - useTerminalConsoleStore, - useVariablesStore, - useSubBlockStore, -} - -// Helper function to reset all stores export const resetAllStores = () => { - // Reset all stores to initial state useWorkflowRegistry.setState({ activeWorkflowId: null, error: null, @@ -214,7 +29,7 @@ export const resetAllStores = () => { }) useWorkflowStore.getState().clear() useSubBlockStore.getState().clear() - useEnvironmentStore.getState().reset() + getQueryClient().removeQueries({ queryKey: environmentKeys.all }) useExecutionStore.getState().reset() useTerminalConsoleStore.setState({ workflowEntries: {}, @@ -223,21 +38,24 @@ export const resetAllStores = () => { isOpen: false, }) consolePersistence.persist() - // Custom tools are managed by React Query cache, not a Zustand store - // Variables store has no tracking to reset; registry hydrates } -// Helper function to log all store states -export const logAllStores = () => { - const state = { - workflow: useWorkflowStore.getState(), - workflowRegistry: useWorkflowRegistry.getState(), - environment: useEnvironmentStore.getState(), - execution: useExecutionStore.getState(), - console: useTerminalConsoleStore.getState(), - subBlock: useSubBlockStore.getState(), - variables: useVariablesStore.getState(), - } +/** + * Clear all user data when signing out. + */ +export async function clearUserData(): Promise { + if (typeof window === 'undefined') return - return state + try { + resetAllStores() + + // Clear localStorage except for essential app settings + const keysToKeep = ['next-favicon', 'theme'] + const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key)) + keysToRemove.forEach((key) => localStorage.removeItem(key)) + + logger.info('User data cleared successfully') + } catch (error) { + logger.error('Error clearing user data:', { error }) + } } diff --git a/apps/sim/stores/panel/variables/store.ts b/apps/sim/stores/panel/variables/store.ts index e9a7db871c5..dbd8145ba55 100644 --- a/apps/sim/stores/panel/variables/store.ts +++ b/apps/sim/stores/panel/variables/store.ts @@ -78,34 +78,6 @@ export const useVariablesStore = create()( error: null, isEditing: null, - async loadForWorkflow(workflowId) { - try { - set({ isLoading: true, error: null }) - const res = await fetch(`/api/workflows/${workflowId}/variables`, { method: 'GET' }) - if (!res.ok) { - const text = await res.text().catch(() => '') - throw new Error(text || `Failed to load variables: ${res.statusText}`) - } - const data = await res.json() - const variables = (data?.data as Record) || {} - set((state) => { - const withoutWorkflow = Object.fromEntries( - Object.entries(state.variables).filter( - (entry): entry is [string, Variable] => entry[1].workflowId !== workflowId - ) - ) - return { - variables: { ...withoutWorkflow, ...variables }, - isLoading: false, - error: null, - } - }) - } catch (e) { - const message = e instanceof Error ? e.message : 'Unknown error' - set({ isLoading: false, error: message }) - } - }, - addVariable: (variable, providedId?: string) => { const id = providedId || crypto.randomUUID() diff --git a/apps/sim/stores/panel/variables/types.ts b/apps/sim/stores/panel/variables/types.ts index c0f7d06d150..7cdfb60d430 100644 --- a/apps/sim/stores/panel/variables/types.ts +++ b/apps/sim/stores/panel/variables/types.ts @@ -23,11 +23,6 @@ export interface VariablesStore { error: string | null isEditing: string | null - /** - * Loads variables for a specific workflow from the API and hydrates the store. - */ - loadForWorkflow: (workflowId: string) => Promise - /** * Adds a new variable with automatic name uniqueness validation * If a variable with the same name exists, it will be suffixed with a number diff --git a/apps/sim/stores/settings/environment/index.ts b/apps/sim/stores/settings/environment/index.ts index 0b13cd29f71..01e93a50d17 100644 --- a/apps/sim/stores/settings/environment/index.ts +++ b/apps/sim/stores/settings/environment/index.ts @@ -1,7 +1 @@ -export { useEnvironmentStore } from './store' -export type { - CachedWorkspaceEnvData, - EnvironmentState, - EnvironmentStore, - EnvironmentVariable, -} from './types' +export type { CachedWorkspaceEnvData, EnvironmentState, EnvironmentVariable } from './types' diff --git a/apps/sim/stores/settings/environment/store.ts b/apps/sim/stores/settings/environment/store.ts deleted file mode 100644 index 99bb11c18fa..00000000000 --- a/apps/sim/stores/settings/environment/store.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createLogger } from '@sim/logger' -import { create } from 'zustand' -import { fetchPersonalEnvironment } from '@/lib/environment/api' -import type { EnvironmentStore, EnvironmentVariable } from './types' - -const logger = createLogger('EnvironmentStore') - -export const useEnvironmentStore = create()((set, get) => ({ - variables: {}, - isLoading: false, - error: null, - - loadEnvironmentVariables: async () => { - try { - set({ isLoading: true, error: null }) - const data = await fetchPersonalEnvironment() - set({ variables: data, isLoading: false }) - } catch (error) { - logger.error('Error loading environment variables:', { error }) - set({ - error: error instanceof Error ? error.message : 'Unknown error', - isLoading: false, - }) - throw error - } - }, - - setVariables: (variables: Record) => { - set({ variables }) - }, - - getAllVariables: () => { - return get().variables - }, - - reset: () => { - set({ - variables: {}, - isLoading: false, - error: null, - }) - }, -})) diff --git a/apps/sim/stores/settings/environment/types.ts b/apps/sim/stores/settings/environment/types.ts index 7e9a575a8dd..8dbb67caf77 100644 --- a/apps/sim/stores/settings/environment/types.ts +++ b/apps/sim/stores/settings/environment/types.ts @@ -15,10 +15,3 @@ export interface EnvironmentState { isLoading: boolean error: string | null } - -export interface EnvironmentStore extends EnvironmentState { - loadEnvironmentVariables: () => Promise - setVariables: (variables: Record) => void - getAllVariables: () => Record - reset: () => void -} diff --git a/apps/sim/stores/variables/store.ts b/apps/sim/stores/variables/store.ts index 497db6550bb..47a9039ae18 100644 --- a/apps/sim/stores/variables/store.ts +++ b/apps/sim/stores/variables/store.ts @@ -1,20 +1,10 @@ -import { createLogger } from '@sim/logger' -import JSON5 from 'json5' -import { v4 as uuidv4 } from 'uuid' import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' -import { normalizeName } from '@/executor/constants' import type { - Variable, VariablesDimensions, + VariablesModalStore, VariablesPosition, - VariablesStore, - VariableType, } from '@/stores/variables/types' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' - -const logger = createLogger('VariablesModalStore') /** * Floating variables modal default dimensions. @@ -25,7 +15,6 @@ const DEFAULT_HEIGHT = 320 /** * Minimum and maximum modal dimensions. - * Kept in sync with the chat modal experience. */ export const MIN_VARIABLES_WIDTH = DEFAULT_WIDTH export const MIN_VARIABLES_HEIGHT = DEFAULT_HEIGHT @@ -110,70 +99,13 @@ export const getVariablesPosition = ( } /** - * Validate a variable's value given its type. Returns an error message or undefined. - */ -function validateVariable(variable: Variable): string | undefined { - try { - switch (variable.type) { - case 'number': { - return Number.isNaN(Number(variable.value)) ? 'Not a valid number' : undefined - } - case 'boolean': { - return !/^(true|false)$/i.test(String(variable.value).trim()) - ? 'Expected "true" or "false"' - : undefined - } - 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' - } - return undefined - } - default: - return undefined - } - } catch (e) { - return e instanceof Error ? e.message : 'Invalid format' - } -} - -/** - * Migrate deprecated type 'string' -> 'plain'. + * UI-only store for the floating variables modal. + * Variable data lives in the panel variables store (`@/stores/panel/variables`). */ -function migrateStringToPlain(variable: Variable): Variable { - if (variable.type !== 'string') return variable - return { ...variable, type: 'plain' as const } -} - -/** - * Floating Variables modal + Variables data store. - */ -export const useVariablesStore = create()( +export const useVariablesStore = create()( devtools( persist( - (set, get) => ({ - // UI + (set) => ({ isOpen: false, position: null, width: DEFAULT_WIDTH, @@ -190,208 +122,17 @@ export const useVariablesStore = create()( ), }), resetPosition: () => set({ position: null }), - - // Data - variables: {}, - isLoading: false, - error: null, - - async loadForWorkflow(workflowId) { - try { - set({ isLoading: true, error: null }) - const res = await fetch(`/api/workflows/${workflowId}/variables`, { method: 'GET' }) - if (!res.ok) { - const text = await res.text().catch(() => '') - throw new Error(text || `Failed to load variables: ${res.statusText}`) - } - const data = await res.json() - const variables = (data?.data as Record) || {} - // Migrate any deprecated types and merge into store (remove other workflow entries) - const migrated: Record = Object.fromEntries( - Object.entries(variables).map(([id, v]) => [id, migrateStringToPlain(v)]) - ) - set((state) => { - const withoutThisWorkflow = Object.fromEntries( - Object.entries(state.variables).filter( - (entry): entry is [string, Variable] => entry[1].workflowId !== workflowId - ) - ) - return { - variables: { ...withoutThisWorkflow, ...migrated }, - isLoading: false, - error: null, - } - }) - } catch (e) { - const message = e instanceof Error ? e.message : 'Unknown error' - set({ isLoading: false, error: message }) - } - }, - - addVariable: (variable, providedId) => { - const id = providedId || uuidv4() - const state = get() - - const workflowVariables = state - .getVariablesByWorkflowId(variable.workflowId) - .map((v) => ({ id: v.id, name: v.name })) - - // Default naming: variableN - 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}` - } - - // Ensure uniqueness - 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) => { - const existing = state.variables[id] - if (!existing) return state - - // Handle name changes: keep references in sync across workflow values - if (update.name !== undefined) { - const oldVariableName = existing.name - const newName = String(update.name).trim() - - if (!newName) { - update = { ...update, name: undefined } - } else if (newName !== oldVariableName) { - const subBlockStore = useSubBlockStore.getState() - const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId - - if (activeWorkflowId) { - const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {} - const updatedWorkflowValues = { ...workflowValues } - - Object.entries(workflowValues).forEach(([blockId, blockValues]) => { - Object.entries(blockValues as Record).forEach( - ([subBlockId, value]) => { - const oldVarName = normalizeName(oldVariableName) - const newVarName = normalizeName(newName) - const regex = new RegExp(``, 'gi') - - updatedWorkflowValues[blockId][subBlockId] = updateReferences( - value, - regex, - `` - ) - - function updateReferences( - val: any, - refRegex: RegExp, - replacement: string - ): any { - if (typeof val === 'string') { - return refRegex.test(val) ? val.replace(refRegex, replacement) : val - } - if (Array.isArray(val)) { - return val.map((item) => updateReferences(item, refRegex, replacement)) - } - if (val !== null && typeof val === 'object') { - const result: Record = { ...val } - for (const key in result) { - result[key] = updateReferences(result[key], refRegex, replacement) - } - return result - } - return val - } - } - ) - }) - - useSubBlockStore.setState({ - workflowValues: { - ...subBlockStore.workflowValues, - [activeWorkflowId]: updatedWorkflowValues, - }, - }) - } - } - } - - // Handle deprecated -> new type migration - if (update.type === 'string') { - update = { ...update, type: 'plain' as VariableType } - } - - const updated: Variable = { - ...existing, - ...update, - validationError: undefined, - } - - // Validate only when type or value changed - if (update.type || update.value !== undefined) { - updated.validationError = validateVariable(updated) - } - - return { - variables: { - ...state.variables, - [id]: updated, - }, - } - }) - }, - - deleteVariable: (id) => { - set((state) => { - if (!state.variables[id]) return state - const { [id]: _deleted, ...rest } = state.variables - return { variables: rest } - }) - }, - - getVariablesByWorkflowId: (workflowId) => { - return Object.values(get().variables).filter((v) => v.workflowId === workflowId) - }, }), { name: 'variables-modal-store', + partialize: (state) => ({ + position: state.position, + width: state.width, + height: state.height, + }), } - ) + ), + { name: 'variables-modal-store' } ) ) diff --git a/apps/sim/stores/variables/types.ts b/apps/sim/stores/variables/types.ts index 610192f49d8..89ed0fd62fb 100644 --- a/apps/sim/stores/variables/types.ts +++ b/apps/sim/stores/variables/types.ts @@ -1,21 +1,3 @@ -/** - * Variable types supported by the variables modal/editor. - * Note: 'string' is deprecated. Use 'plain' for freeform text values instead. - */ -export type VariableType = 'plain' | 'number' | 'boolean' | 'object' | 'array' | 'string' - -/** - * Workflow-scoped variable model. - */ -export interface Variable { - id: string - workflowId: string - name: string - type: VariableType - value: unknown - validationError?: string -} - /** * 2D position used by the floating variables modal. */ @@ -33,11 +15,10 @@ export interface VariablesDimensions { } /** - * Public store interface for variables editor/modal. - * Combines UI state of the floating modal and the variables data/actions. + * UI-only store interface for the floating variables modal. + * Variable data lives in the panel variables store (`@/stores/panel/variables`). */ -export interface VariablesStore { - // UI State +export interface VariablesModalStore { isOpen: boolean position: VariablesPosition | null width: number @@ -46,16 +27,4 @@ export interface VariablesStore { setPosition: (position: VariablesPosition) => void setDimensions: (dimensions: VariablesDimensions) => void resetPosition: () => void - - // Data - variables: Record - isLoading: boolean - error: string | null - - // Actions - loadForWorkflow: (workflowId: string) => Promise - addVariable: (variable: Omit, providedId?: string) => string - updateVariable: (id: string, update: Partial>) => void - deleteVariable: (id: string) => void - getVariablesByWorkflowId: (workflowId: string) => Variable[] } diff --git a/apps/sim/tools/utils.test.ts b/apps/sim/tools/utils.test.ts index 43a5531da91..9d8fa28f2f1 100644 --- a/apps/sim/tools/utils.test.ts +++ b/apps/sim/tools/utils.test.ts @@ -21,24 +21,23 @@ vi.mock('@/lib/core/security/input-validation.server', () => ({ secureFetchWithPinnedIP: vi.fn(), })) -vi.mock('@/stores/settings/environment', () => { - const mockStore = { - getAllVariables: vi.fn().mockReturnValue({ - API_KEY: { value: 'mock-api-key' }, - BASE_URL: { value: 'https://example.com' }, - }), - } - - return { - useEnvironmentStore: { - getState: vi.fn().mockImplementation(() => mockStore), - }, - } -}) +const { mockGetQueryData } = vi.hoisted(() => ({ + mockGetQueryData: vi.fn(), +})) + +vi.mock('@/app/_shell/providers/get-query-client', () => ({ + getQueryClient: () => ({ + getQueryData: mockGetQueryData, + }), +})) const originalWindow = global.window beforeEach(() => { global.window = {} as any + mockGetQueryData.mockReturnValue({ + API_KEY: { key: 'API_KEY', value: 'mock-api-key' }, + BASE_URL: { key: 'BASE_URL', value: 'https://example.com' }, + }) }) afterEach(() => { @@ -651,15 +650,8 @@ describe('createParamSchema', () => { }) describe('getClientEnvVars', () => { - it.concurrent('should return environment variables from store in browser environment', () => { - const mockStoreGetter = () => ({ - getAllVariables: () => ({ - API_KEY: { value: 'mock-api-key' }, - BASE_URL: { value: 'https://example.com' }, - }), - }) - - const result = getClientEnvVars(mockStoreGetter) + it('should return environment variables from React Query cache in browser environment', () => { + const result = getClientEnvVars() expect(result).toEqual({ API_KEY: 'mock-api-key', @@ -667,7 +659,7 @@ describe('getClientEnvVars', () => { }) }) - it.concurrent('should return empty object in server environment', () => { + it('should return empty object in server environment', () => { global.window = undefined as any const result = getClientEnvVars() @@ -677,7 +669,7 @@ describe('getClientEnvVars', () => { }) describe('createCustomToolRequestBody', () => { - it.concurrent('should create request body function for client-side execution', () => { + it('should create request body function for client-side execution', () => { const customTool = { code: 'return a + b', schema: { @@ -687,14 +679,7 @@ describe('createCustomToolRequestBody', () => { }, } - const mockStoreGetter = () => ({ - getAllVariables: () => ({ - API_KEY: { value: 'mock-api-key' }, - BASE_URL: { value: 'https://example.com' }, - }), - }) - - const bodyFn = createCustomToolRequestBody(customTool, true, undefined, mockStoreGetter) + const bodyFn = createCustomToolRequestBody(customTool, true) const result = bodyFn({ a: 5, b: 3 }) expect(result).toEqual({ diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 2f944c18bd4..534dc51797c 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -1,7 +1,9 @@ import { createLogger } from '@sim/logger' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' +import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { CustomToolDefinition } from '@/hooks/queries/custom-tools' -import { useEnvironmentStore } from '@/stores/settings/environment' +import { environmentKeys } from '@/hooks/queries/environment' +import type { EnvironmentVariable } from '@/stores/settings/environment' import { tools } from '@/tools/registry' import type { ToolConfig } from '@/tools/types' @@ -215,20 +217,20 @@ export function createParamSchema(customTool: any): Record { } /** - * Get environment variables from store (client-side only) - * @param getStore Optional function to get the store (useful for testing) + * Get environment variables from React Query cache (client-side only) */ -export function getClientEnvVars(getStore?: () => any): Record { +export function getClientEnvVars(): Record { if (typeof window === 'undefined') return {} try { - // Allow injecting the store for testing - const envStore = getStore ? getStore() : useEnvironmentStore.getState() - const allEnvVars = envStore.getAllVariables() + const allEnvVars = + getQueryClient().getQueryData>( + environmentKeys.personal() + ) ?? {} // Convert environment variables to a simple key-value object return Object.entries(allEnvVars).reduce( - (acc, [key, variable]: [string, any]) => { + (acc, [key, variable]) => { acc[key] = variable.value return acc }, @@ -245,20 +247,14 @@ export function getClientEnvVars(getStore?: () => any): Record { * @param customTool The custom tool configuration * @param isClient Whether running on client side * @param workflowId Optional workflow ID for server-side - * @param getStore Optional function to get the store (useful for testing) */ -export function createCustomToolRequestBody( - customTool: any, - isClient = true, - workflowId?: string, - getStore?: () => any -) { +export function createCustomToolRequestBody(customTool: any, isClient = true, workflowId?: string) { return (params: Record) => { // Get environment variables - try multiple sources in order of preference: // 1. envVars parameter (passed from provider/agent context) // 2. Client-side store (if running in browser) // 3. Empty object (fallback) - const envVars = params.envVars || (isClient ? getClientEnvVars(getStore) : {}) + const envVars = params.envVars || (isClient ? getClientEnvVars() : {}) // Get workflow variables from params (passed from execution context) const workflowVariables = params.workflowVariables || {}