improvement(stores): remove deployment state from Zustand in favor of React Query (#3923)

This commit is contained in:
Waleed
2026-04-03 17:44:10 -07:00
committed by GitHub
parent e8f7fe0989
commit 65fc138bfc
28 changed files with 64 additions and 470 deletions
@@ -304,7 +304,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
loops: {},
parallels: {},
isDeployed: true,
deploymentStatuses: { production: 'deployed' },
},
}
@@ -349,7 +348,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
loops: {},
parallels: {},
isDeployed: true,
deploymentStatuses: { production: 'deployed' },
lastSaved: 1640995200000,
},
},
@@ -370,7 +368,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
loops: {},
parallels: {},
isDeployed: true,
deploymentStatuses: { production: 'deployed' },
lastSaved: 1640995200000,
}),
}
@@ -473,7 +470,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
edges: undefined,
loops: null,
parallels: undefined,
deploymentStatuses: null,
},
}
@@ -508,7 +504,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
loops: {},
parallels: {},
isDeployed: false,
deploymentStatuses: {},
lastSaved: 1640995200000,
})
})
@@ -768,10 +763,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
parallel1: { branches: ['branch1', 'branch2'] },
},
isDeployed: true,
deploymentStatuses: {
production: 'deployed',
staging: 'pending',
},
deployedAt: '2024-01-01T10:00:00.000Z',
},
}
@@ -816,10 +807,6 @@ describe('Copilot Checkpoints Revert API Route', () => {
parallel1: { branches: ['branch1', 'branch2'] },
},
isDeployed: true,
deploymentStatuses: {
production: 'deployed',
staging: 'pending',
},
deployedAt: '2024-01-01T10:00:00.000Z',
lastSaved: 1640995200000,
})
@@ -82,7 +82,6 @@ export async function POST(request: NextRequest) {
loops: checkpointState?.loops || {},
parallels: checkpointState?.parallels || {},
isDeployed: checkpointState?.isDeployed || false,
deploymentStatuses: checkpointState?.deploymentStatuses || {},
lastSaved: Date.now(),
...(checkpointState?.deployedAt &&
checkpointState.deployedAt !== null &&
@@ -79,7 +79,6 @@ export async function POST(
loops: deployedState.loops || {},
parallels: deployedState.parallels || {},
lastSaved: Date.now(),
deploymentStatuses: deployedState.deploymentStatuses || {},
})
if (!saveResult.success) {
-2
View File
@@ -89,7 +89,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const finalWorkflowData = {
...workflowData,
state: {
deploymentStatuses: {},
blocks: normalizedData.blocks,
edges: normalizedData.edges,
loops: normalizedData.loops,
@@ -115,7 +114,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
const emptyWorkflowData = {
...workflowData,
state: {
deploymentStatuses: {},
blocks: {},
edges: [],
loops: {},
@@ -108,8 +108,6 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
lastUpdate: input.lastUpdate,
metadata: input.metadata,
variables: input.variables,
deploymentStatuses: input.deploymentStatuses,
needsRedeployment: input.needsRedeployment,
dragStartPosition: input.dragStartPosition ?? null,
}
@@ -1407,17 +1407,6 @@ export function useChat(
const output = tc.result?.output as Record<string, unknown> | undefined
const deployedWorkflowId = (output?.workflowId as string) ?? undefined
if (deployedWorkflowId && typeof output?.isDeployed === 'boolean') {
const isDeployed = output.isDeployed as boolean
const serverDeployedAt = output.deployedAt
? new Date(output.deployedAt as string)
: undefined
useWorkflowRegistry
.getState()
.setDeploymentStatus(
deployedWorkflowId,
isDeployed,
isDeployed ? (serverDeployedAt ?? new Date()) : undefined
)
queryClient.invalidateQueries({
queryKey: deploymentKeys.info(deployedWorkflowId),
})
@@ -111,8 +111,6 @@ function normalizeWorkflowState(input?: any): WorkflowState | null {
lastUpdate: input.lastUpdate,
metadata: input.metadata,
variables: input.variables,
deploymentStatuses: input.deploymentStatuses,
needsRedeployment: input.needsRedeployment,
dragStartPosition: input.dragStartPosition ?? null,
}
@@ -40,7 +40,6 @@ import { useWorkflowMap } from '@/hooks/queries/workflows'
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -90,10 +89,7 @@ export function DeployModal({
const params = useParams()
const workspaceId = params?.workspaceId as string
const { navigateToSettings } = useSettingsNavigation()
const deploymentStatus = useWorkflowRegistry((state) =>
state.getWorkflowDeploymentStatus(workflowId)
)
const isDeployed = deploymentStatus?.isDeployed ?? isDeployedProp
const isDeployed = isDeployedProp
const { data: workflowMap = {} } = useWorkflowMap(workspaceId)
const workflowMetadata = workflowId ? workflowMap[workflowId] : undefined
const workflowWorkspaceId = workflowMetadata?.workspaceId ?? null
@@ -381,8 +377,6 @@ export function DeployModal({
invalidateDeploymentQueries(queryClient, workflowId)
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
if (chatSuccessTimeoutRef.current) {
clearTimeout(chatSuccessTimeoutRef.current)
}
@@ -9,7 +9,7 @@ import {
useDeployment,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
import { useDeployedWorkflowState } from '@/hooks/queries/deployments'
import { useDeployedWorkflowState, useDeploymentInfo } from '@/hooks/queries/deployments'
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -25,10 +25,10 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
const isRegistryLoading = hydrationPhase === 'idle' || hydrationPhase === 'state-loading'
const { hasBlocks } = useCurrentWorkflow()
const deploymentStatus = useWorkflowRegistry((state) =>
state.getWorkflowDeploymentStatus(activeWorkflowId)
)
const isDeployed = deploymentStatus?.isDeployed || false
const { data: deploymentInfo } = useDeploymentInfo(activeWorkflowId, {
enabled: !isRegistryLoading,
})
const isDeployed = deploymentInfo?.isDeployed ?? false
const isDeployedStateEnabled = Boolean(activeWorkflowId) && isDeployed && !isRegistryLoading
const {
@@ -2,7 +2,6 @@ import { useMemo } from 'react'
import type { Edge } from 'reactflow'
import { useShallow } from 'zustand/react/shallow'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
@@ -16,8 +15,6 @@ export interface CurrentWorkflow {
loops: Record<string, Loop>
parallels: Record<string, Parallel>
lastSaved?: number
deploymentStatuses?: Record<string, DeploymentStatus>
needsRedeployment?: boolean
// Mode information
isDiffMode: boolean
@@ -48,8 +45,6 @@ export function useCurrentWorkflow(): CurrentWorkflow {
loops: state.loops,
parallels: state.parallels,
lastSaved: state.lastSaved,
deploymentStatuses: state.deploymentStatuses,
needsRedeployment: state.needsRedeployment,
}))
)
@@ -78,8 +73,6 @@ export function useCurrentWorkflow(): CurrentWorkflow {
loops: activeWorkflow.loops || {},
parallels: activeWorkflow.parallels || {},
lastSaved: activeWorkflow.lastSaved,
deploymentStatuses: activeWorkflow.deploymentStatuses,
needsRedeployment: activeWorkflow.needsRedeployment,
// Mode information - update to reflect ready state
isDiffMode: hasActiveDiff && isShowingDiff,
@@ -124,8 +124,7 @@ export async function applyAutoLayoutAndUpdateStore(
try {
useWorkflowStore.getState().updateLastSaved()
const { deploymentStatuses, needsRedeployment, dragStartPosition, ...stateToSave } =
newWorkflowState
const { dragStartPosition, ...stateToSave } = newWorkflowState
const cleanedWorkflowState = {
...stateToSave,
@@ -424,7 +424,6 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
loops: workflowState.loops || {},
parallels: workflowState.parallels || {},
lastSaved: workflowState.lastSaved || Date.now(),
deploymentStatuses: workflowState.deploymentStatuses || {},
})
useSubBlockStore.setState((state: any) => ({
+19 -41
View File
@@ -5,7 +5,6 @@ import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tansta
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
import { fetchDeploymentVersionState } from '@/hooks/queries/utils/fetch-deployment-version-state'
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('DeploymentQueries')
@@ -321,7 +320,6 @@ interface DeployWorkflowResult {
*/
export function useDeployWorkflow() {
const queryClient = useQueryClient()
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
return useMutation({
mutationFn: async ({
@@ -351,18 +349,12 @@ export function useDeployWorkflow() {
warnings: data.warnings,
}
},
onSuccess: (data, variables) => {
logger.info('Workflow deployed successfully', { workflowId: variables.workflowId })
setDeploymentStatus(
variables.workflowId,
data.isDeployed,
data.deployedAt ? new Date(data.deployedAt) : undefined,
data.apiKey
)
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(variables.workflowId, false)
onSettled: (_data, error, variables) => {
if (error) {
logger.error('Failed to deploy workflow', { error })
} else {
logger.info('Workflow deployed successfully', { workflowId: variables.workflowId })
}
return Promise.all([
invalidateDeploymentQueries(queryClient, variables.workflowId),
queryClient.invalidateQueries({
@@ -370,9 +362,6 @@ export function useDeployWorkflow() {
}),
])
},
onError: (error) => {
logger.error('Failed to deploy workflow', { error })
},
})
}
@@ -389,7 +378,6 @@ interface UndeployWorkflowVariables {
*/
export function useUndeployWorkflow() {
const queryClient = useQueryClient()
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
return useMutation({
mutationFn: async ({ workflowId }: UndeployWorkflowVariables): Promise<void> => {
@@ -402,11 +390,12 @@ export function useUndeployWorkflow() {
throw new Error(errorData.error || 'Failed to undeploy workflow')
}
},
onSuccess: (_, variables) => {
logger.info('Workflow undeployed successfully', { workflowId: variables.workflowId })
setDeploymentStatus(variables.workflowId, false)
onSettled: (_data, error, variables) => {
if (error) {
logger.error('Failed to undeploy workflow', { error })
} else {
logger.info('Workflow undeployed successfully', { workflowId: variables.workflowId })
}
return Promise.all([
invalidateDeploymentQueries(queryClient, variables.workflowId),
queryClient.invalidateQueries({
@@ -414,9 +403,6 @@ export function useUndeployWorkflow() {
}),
])
},
onError: (error) => {
logger.error('Failed to undeploy workflow', { error })
},
})
}
@@ -613,7 +599,6 @@ interface ActivateVersionResult {
*/
export function useActivateDeploymentVersion() {
const queryClient = useQueryClient()
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
return useMutation({
mutationFn: async ({
@@ -663,20 +648,13 @@ export function useActivateDeploymentVersion() {
)
}
},
onSuccess: (data, variables) => {
logger.info('Deployment version activated', {
workflowId: variables.workflowId,
version: variables.version,
})
setDeploymentStatus(
variables.workflowId,
true,
data.deployedAt ? new Date(data.deployedAt) : undefined,
data.apiKey
)
},
onSettled: (_, __, variables) => {
onSettled: (_data, error, variables) => {
if (!error) {
logger.info('Deployment version activated', {
workflowId: variables.workflowId,
version: variables.version,
})
}
return invalidateDeploymentQueries(queryClient, variables.workflowId)
},
})
@@ -1,39 +0,0 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { getQueryDataMock } = vi.hoisted(() => ({
getQueryDataMock: vi.fn(),
}))
vi.mock('@/app/_shell/providers/get-query-client', () => ({
getQueryClient: vi.fn(() => ({
getQueryData: getQueryDataMock,
})),
}))
import { getCustomTool, getCustomTools } from '@/hooks/queries/utils/custom-tool-cache'
describe('custom tool cache helpers', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('reads workspace-scoped custom tools from the cache', () => {
const tools = [{ id: 'tool-1', title: 'Weather', schema: {}, code: '', workspaceId: 'ws-1' }]
getQueryDataMock.mockReturnValue(tools)
expect(getCustomTools('ws-1')).toBe(tools)
expect(getQueryDataMock).toHaveBeenCalledWith(['customTools', 'list', 'ws-1'])
})
it('resolves custom tools by id or title', () => {
getQueryDataMock.mockReturnValue([
{ id: 'tool-1', title: 'Weather', schema: {}, code: '', workspaceId: 'ws-1' },
])
expect(getCustomTool('tool-1', 'ws-1')?.title).toBe('Weather')
expect(getCustomTool('Weather', 'ws-1')?.id).toBe('tool-1')
})
})
@@ -1,23 +0,0 @@
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import type { CustomToolDefinition } from '@/hooks/queries/custom-tools'
import { customToolsKeys } from '@/hooks/queries/utils/custom-tool-keys'
/**
* Reads custom tools for a workspace directly from the React Query cache.
*/
export function getCustomTools(workspaceId: string): CustomToolDefinition[] {
return (
getQueryClient().getQueryData<CustomToolDefinition[]>(customToolsKeys.list(workspaceId)) ?? []
)
}
/**
* Resolves a custom tool from the cache by id or title.
*/
export function getCustomTool(
identifier: string,
workspaceId: string
): CustomToolDefinition | undefined {
const tools = getCustomTools(workspaceId)
return tools.find((tool) => tool.id === identifier || tool.title === identifier)
}
@@ -562,7 +562,6 @@ export function useCollaborativeWorkflow() {
loops: workflowData.state.loops || {},
parallels: workflowData.state.parallels || {},
lastSaved: workflowData.state.lastSaved || Date.now(),
deploymentStatuses: workflowData.state.deploymentStatuses || {},
})
// Update subblock store with reverted values
@@ -40,7 +40,11 @@ import {
XCircle,
Zap,
} from 'lucide-react'
import { getCustomTool } from '@/hooks/queries/utils/custom-tool-cache'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import type { CustomToolDefinition } from '@/hooks/queries/custom-tools'
import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments'
import { deploymentKeys } from '@/hooks/queries/deployments'
import { customToolsKeys } from '@/hooks/queries/utils/custom-tool-keys'
import { getWorkflowById } from '@/hooks/queries/utils/workflow-cache'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -445,7 +449,8 @@ const META_deploy_api: ToolMetadata = {
const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId
const isAlreadyDeployed = workflowId
? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed
? (getQueryClient().getQueryData<WorkflowDeploymentInfo>(deploymentKeys.info(workflowId))
?.isDeployed ?? false)
: false
let actionText = action
@@ -1053,7 +1058,11 @@ const META_manage_custom_tool: ToolMetadata = {
let toolName = params?.schema?.function?.name
if (!toolName && params?.toolId && workspaceId) {
try {
const tool = getCustomTool(params.toolId, workspaceId)
const tools =
getQueryClient().getQueryData<CustomToolDefinition[]>(
customToolsKeys.list(workspaceId)
) ?? []
const tool = tools.find((t) => t.id === params.toolId || t.title === params.toolId)
toolName = tool?.schema?.function?.name
} catch {
// Ignore errors accessing cache
+1 -2
View File
@@ -2,10 +2,9 @@ import type { Edge } from 'reactflow'
import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types'
import type { ParentIteration, SerializableExecutionState } from '@/executor/execution/types'
import type { BlockLog, NormalizedBlockOutput } from '@/executor/types'
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
import type { Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
export type { WorkflowState, Loop, Parallel, DeploymentStatus }
export type { WorkflowState, Loop, Parallel }
export type WorkflowEdge = Edge
export type { NormalizedBlockOutput, BlockLog }
-2
View File
@@ -123,8 +123,6 @@ export function buildDefaultWorkflowArtifacts(): DefaultWorkflowArtifacts {
loops: {},
parallels: {},
lastSaved: Date.now(),
deploymentStatuses: {},
needsRedeployment: false,
}
return {
@@ -985,7 +985,6 @@ describe('Database Helpers', () => {
edges: loadedState!.edges,
loops: {},
parallels: {},
deploymentStatuses: {},
}
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
-1
View File
@@ -186,7 +186,6 @@ export async function getWorkflowState(workflowId: string) {
if (normalizedData) {
const finalState = {
deploymentStatuses: {},
hasActiveWebhook: false,
blocks: normalizedData.blocks,
edges: normalizedData.edges,
-1
View File
@@ -203,7 +203,6 @@ export const resetAllStores = () => {
useWorkflowRegistry.setState({
activeWorkflowId: null,
error: null,
deploymentStatuses: {},
hydration: {
phase: 'idle',
workspaceId: null,
+1 -104
View File
@@ -3,7 +3,7 @@ import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
const logger = createLogger('Workflows')
@@ -30,9 +30,6 @@ export function getWorkflowWithValues(workflowId: string, workspaceId: string) {
return null
}
// Get deployment status from registry
const deploymentStatus = useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)
// Use the current state from the store (only available for active workflow)
const workflowState: WorkflowState = useWorkflowStore.getState().getWorkflowState()
@@ -52,110 +49,10 @@ export function getWorkflowWithValues(workflowId: string, workspaceId: string) {
loops: workflowState.loops,
parallels: workflowState.parallels,
lastSaved: workflowState.lastSaved,
// Get deployment fields from registry for API compatibility
isDeployed: deploymentStatus?.isDeployed || false,
deployedAt: deploymentStatus?.deployedAt,
},
}
}
/**
* Get a specific block with its subblock values merged in
* @param blockId ID of the block to retrieve
* @returns The block with merged subblock values or null if not found
*/
export function getBlockWithValues(blockId: string): BlockState | null {
const workflowState = useWorkflowStore.getState()
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId || !workflowState.blocks[blockId]) return null
const mergedBlocks = mergeSubblockState(workflowState.blocks, activeWorkflowId, blockId)
return mergedBlocks[blockId] || null
}
/**
* Get all workflows with their values merged
* Note: Since localStorage has been removed, this only includes the active workflow state
* @param workspaceId Workspace containing the workflow metadata
* @returns An object containing workflows, with state only for the active workflow
*/
export function getAllWorkflowsWithValues(workspaceId: string) {
const workflows = getWorkflows(workspaceId)
const result: Record<
string,
{
id: string
name: string
description?: string
color: string
folderId?: string | null
workspaceId?: string
apiKey?: string
state: WorkflowState & { isDeployed: boolean; deployedAt?: Date }
}
> = {}
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
const currentState = useWorkflowStore.getState()
// Only sync the active workflow to ensure we always send valid state data
const activeMetadata = activeWorkflowId
? workflows.find((w) => w.id === activeWorkflowId)
: undefined
if (activeWorkflowId && activeMetadata) {
const metadata = activeMetadata
// Get deployment status from registry
const deploymentStatus = useWorkflowRegistry
.getState()
.getWorkflowDeploymentStatus(activeWorkflowId)
// Ensure state has all required fields for Zod validation
const workflowState: WorkflowState = {
...useWorkflowStore.getState().getWorkflowState(),
// Ensure fallback values for safer handling
blocks: currentState.blocks || {},
edges: currentState.edges || [],
loops: currentState.loops || {},
parallels: currentState.parallels || {},
lastSaved: currentState.lastSaved || Date.now(),
}
// Merge the subblock values for this specific workflow
const mergedBlocks = mergeSubblockState(workflowState.blocks, activeWorkflowId)
// Include the API key in the state if it exists in the deployment status
const apiKey = deploymentStatus?.apiKey
result[activeWorkflowId] = {
id: activeWorkflowId,
name: metadata.name,
description: metadata.description,
color: metadata.color || '#3972F6',
folderId: metadata.folderId,
state: {
blocks: mergedBlocks,
edges: workflowState.edges,
loops: workflowState.loops,
parallels: workflowState.parallels,
lastSaved: workflowState.lastSaved,
// Get deployment fields from registry for API compatibility
isDeployed: deploymentStatus?.isDeployed || false,
deployedAt: deploymentStatus?.deployedAt,
},
// Include API key if available
apiKey,
}
// Only include workspaceId if it's not null/undefined
if (metadata.workspaceId) {
result[activeWorkflowId].workspaceId = metadata.workspaceId
}
}
return result
}
export { useWorkflowRegistry } from '@/stores/workflows/registry/store'
export type { WorkflowMetadata } from '@/stores/workflows/registry/types'
export { useSubBlockStore } from '@/stores/workflows/subblock/store'
+16 -94
View File
@@ -3,14 +3,12 @@ import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
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 type {
DeploymentStatus,
HydrationState,
WorkflowRegistry,
} from '@/stores/workflows/registry/types'
import type { HydrationState, WorkflowRegistry } from '@/stores/workflows/registry/types'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { getUniqueBlockName, regenerateBlockIds } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -34,7 +32,6 @@ function resetWorkflowStores() {
edges: [],
loops: {},
parallels: {},
deploymentStatuses: {},
lastSaved: Date.now(),
})
@@ -48,7 +45,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
(set, get) => ({
activeWorkflowId: null,
error: null,
deploymentStatuses: {},
hydration: initialHydration,
clipboard: null,
pendingSelection: null,
@@ -61,7 +57,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
set({
activeWorkflowId: null,
deploymentStatuses: {},
error: null,
hydration: {
phase: 'idle',
@@ -73,74 +68,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
})
},
getWorkflowDeploymentStatus: (workflowId: string | null): DeploymentStatus | null => {
if (!workflowId) {
workflowId = get().activeWorkflowId
if (!workflowId) return null
}
const { deploymentStatuses = {} } = get()
if (deploymentStatuses[workflowId]) {
return deploymentStatuses[workflowId]
}
return null
},
setDeploymentStatus: (
workflowId: string | null,
isDeployed: boolean,
deployedAt?: Date,
apiKey?: string
) => {
if (!workflowId) {
workflowId = get().activeWorkflowId
if (!workflowId) return
}
set((state) => ({
deploymentStatuses: {
...state.deploymentStatuses,
[workflowId as string]: {
isDeployed,
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
apiKey,
needsRedeployment: isDeployed
? false
: (state.deploymentStatuses?.[workflowId as string]?.needsRedeployment ?? false),
},
},
}))
},
setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => {
if (!workflowId) {
workflowId = get().activeWorkflowId
if (!workflowId) return
}
set((state) => {
const deploymentStatuses = state.deploymentStatuses || {}
const currentStatus = deploymentStatuses[workflowId as string] || { isDeployed: false }
return {
deploymentStatuses: {
...deploymentStatuses,
[workflowId as string]: {
...currentStatus,
needsRedeployment,
},
},
}
})
const { activeWorkflowId } = get()
if (workflowId === activeWorkflowId) {
useWorkflowStore.getState().setNeedsRedeploymentFlag(needsRedeployment)
}
},
loadWorkflowState: async (workflowId: string) => {
const workspaceId = get().hydration.workspaceId
if (!workspaceId) {
@@ -170,20 +97,19 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
}
const workflowData = (await response.json()).data
const nextDeploymentStatuses =
workflowData?.isDeployed || workflowData?.deployedAt
? {
...get().deploymentStatuses,
[workflowId]: {
isDeployed: workflowData.isDeployed || false,
deployedAt: workflowData.deployedAt
? new Date(workflowData.deployedAt)
: undefined,
apiKey: workflowData.apiKey || undefined,
needsRedeployment: false,
},
}
: get().deploymentStatuses
if (workflowData?.isDeployed !== undefined) {
getQueryClient().setQueryData<WorkflowDeploymentInfo>(
deploymentKeys.info(workflowId),
(prev) => ({
isDeployed: workflowData.isDeployed ?? false,
deployedAt: workflowData.deployedAt ?? null,
apiKey: workflowData.apiKey ?? prev?.apiKey ?? null,
needsRedeployment: prev?.needsRedeployment ?? false,
isPublicApi: prev?.isPublicApi ?? false,
})
)
}
let workflowState: WorkflowState
@@ -195,7 +121,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
loops: workflowData.state.loops || {},
parallels: workflowData.state.parallels || {},
lastSaved: Date.now(),
deploymentStatuses: nextDeploymentStatuses,
}
} else {
workflowState = {
@@ -204,7 +129,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
edges: [],
loops: {},
parallels: {},
deploymentStatuses: nextDeploymentStatuses,
lastSaved: Date.now(),
}
@@ -250,7 +174,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
set((state) => ({
activeWorkflowId: workflowId,
error: null,
deploymentStatuses: nextDeploymentStatuses,
hydration: {
phase: 'ready',
workspaceId: state.hydration.workspaceId,
@@ -367,7 +290,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
set({
activeWorkflowId: null,
deploymentStatuses: {},
error: null,
hydration: initialHydration,
clipboard: null,
@@ -1,13 +1,6 @@
import type { Edge } from 'reactflow'
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
export interface DeploymentStatus {
isDeployed: boolean
deployedAt?: Date
apiKey?: string
needsRedeployment?: boolean
}
export interface ClipboardData {
blocks: Record<string, BlockState>
edges: Edge[]
@@ -45,7 +38,6 @@ export interface HydrationState {
export interface WorkflowRegistryState {
activeWorkflowId: string | null
error: string | null
deploymentStatuses: Record<string, DeploymentStatus>
hydration: HydrationState
clipboard: ClipboardData | null
pendingSelection: string[] | null
@@ -57,14 +49,6 @@ export interface WorkflowRegistryActions {
switchToWorkspace: (id: string) => void
markWorkflowCreating: (workflowId: string) => void
markWorkflowCreated: (workflowId: string | null) => void
getWorkflowDeploymentStatus: (workflowId: string | null) => DeploymentStatus | null
setDeploymentStatus: (
workflowId: string | null,
isDeployed: boolean,
deployedAt?: Date,
apiKey?: string
) => void
setWorkflowNeedsRedeployment: (workflowId: string | null, needsRedeployment: boolean) => void
copyBlocks: (blockIds: string[]) => void
preparePasteData: (positionOffset?: { x: number; y: number }) => {
blocks: Record<string, BlockState>
@@ -107,8 +107,6 @@ const initialState = {
loops: {},
parallels: {},
lastSaved: undefined,
deploymentStatuses: {},
needsRedeployment: false,
}
export const useWorkflowStore = create<WorkflowStore>()(
@@ -116,10 +114,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
(set, get) => ({
...initialState,
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => {
set({ needsRedeployment })
},
setCurrentWorkflowId: (currentWorkflowId) => {
set({ currentWorkflowId })
},
@@ -540,8 +534,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
loops: state.loops,
parallels: state.parallels,
lastSaved: state.lastSaved,
deploymentStatuses: state.deploymentStatuses,
needsRedeployment: state.needsRedeployment,
}
},
replaceWorkflowState: (
@@ -580,11 +572,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
edges: nextEdges,
loops: nextLoops,
parallels: nextParallels,
deploymentStatuses: nextState.deploymentStatuses || state.deploymentStatuses,
needsRedeployment:
nextState.needsRedeployment !== undefined
? nextState.needsRedeployment
: state.needsRedeployment,
lastSaved:
options?.updateLastSaved === true
? Date.now()
@@ -1141,67 +1128,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
}))
},
revertToDeployedState: async (deployedState: WorkflowState) => {
const activeWorkflowId = get().currentWorkflowId
if (!activeWorkflowId) {
logger.error('Cannot revert: no active workflow ID')
return
}
const deploymentStatus = get().deploymentStatuses?.[activeWorkflowId]
get().replaceWorkflowState({
...deployedState,
needsRedeployment: false,
deploymentStatuses: {
...get().deploymentStatuses,
...(deploymentStatus ? { [activeWorkflowId]: deploymentStatus } : {}),
},
})
const values: Record<string, Record<string, any>> = {}
Object.entries(deployedState.blocks).forEach(([blockId, block]) => {
values[blockId] = {}
Object.entries(block.subBlocks || {}).forEach(([subBlockId, subBlock]) => {
values[blockId][subBlockId] = subBlock.value
})
})
useSubBlockStore.setState({
workflowValues: {
...useSubBlockStore.getState().workflowValues,
[activeWorkflowId]: values,
},
})
get().updateLastSaved()
// Call API to persist the revert to normalized tables
try {
const response = await fetch(
`/api/workflows/${activeWorkflowId}/deployments/active/revert`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json()
logger.error('Failed to persist revert to deployed state:', errorData.error)
// Don't throw error to avoid breaking the UI, but log it
} else {
logger.info('Successfully persisted revert to deployed state')
}
} catch (error) {
logger.error('Error calling revert to deployed API:', error)
// Don't throw error to avoid breaking the UI
}
},
toggleBlockAdvancedMode: (id: string) => {
const block = get().blocks[id]
if (!block) return
@@ -1,6 +1,5 @@
import type { Edge } from 'reactflow'
import type { OutputFieldDefinition, SubBlockType } from '@/blocks/types'
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
export const SUBFLOW_TYPES = {
LOOP: 'loop',
@@ -173,8 +172,6 @@ export interface WorkflowState {
exportedAt?: string
}
variables?: Record<string, Variable>
deploymentStatuses?: Record<string, DeploymentStatus>
needsRedeployment?: boolean
dragStartPosition?: DragStartPosition | null
}
@@ -228,8 +225,6 @@ export interface WorkflowActions {
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => void
generateLoopBlocks: () => Record<string, Loop>
generateParallelBlocks: () => Record<string, Parallel>
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
revertToDeployedState: (deployedState: WorkflowState) => void
toggleBlockAdvancedMode: (id: string) => void
setDragStartPosition: (position: DragStartPosition | null) => void
getDragStartPosition: () => DragStartPosition | null
+8 -9
View File
@@ -193,8 +193,8 @@ vi.mock('@/tools/registry', () => {
return { tools: mockTools }
})
// Mock custom tools - define mock data inside factory function
vi.mock('@/hooks/queries/utils/custom-tool-cache', () => {
// Mock query client for custom tool cache reads
vi.mock('@/app/_shell/providers/get-query-client', () => {
const mockCustomTool = {
id: 'custom-tool-123',
title: 'Custom Weather Tool',
@@ -214,13 +214,12 @@ vi.mock('@/hooks/queries/utils/custom-tool-cache', () => {
},
}
return {
getCustomTool: (toolId: string) => {
if (toolId === 'custom-tool-123') {
return mockCustomTool
}
return undefined
},
getCustomTools: () => [mockCustomTool],
getQueryClient: () => ({
getQueryData: (key: string[]) => {
if (key[0] === 'customTools') return [mockCustomTool]
return undefined
},
}),
}
})