mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(workflow): narrow zustand selectors and optimize log tree builds (#4378)
* improvement(workflow): narrow zustand selectors and optimize log tree builds - Add useIsCurrentWorkflowExecuting selector; swap broad useCurrentWorkflowExecution in action-bar/chat - Use useShallow for execution-state subset in useWorkflowExecution - Drop useCallback-wrapped object selectors in diff-controls and use-block-state (use primitive selectors) - Stabilize EMPTY_SUBBLOCK_VALUES constant for empty-workflow selector results in api-info-modal, mcp, tag-dropdown - O(n) Map lookups in buildEntryTree (was O(n^2) filter scans) - Skip expanded-paths reset when structured-output data is content-stable - Extract ReactFlow constants out of workflow.tsx into workflow-constants.ts * fix(workflow): seed structured-output JSON ref and centralize EMPTY_SUBBLOCK_VALUES - Use null sentinel for prevDataJsonRef and lazily stringify prevDataRef on first compare so the optimization holds on the first stream refresh after mount - Export EMPTY_SUBBLOCK_VALUES from the subblock store; remove three duplicated module constants * chore(ci): bump api-validation route baseline to 717 PR #4373 added apps/sim/app/api/table/[tableId]/export/route.ts but didn't update the audit baseline, so unrelated PRs fail check:api-validation:strict. Bump baseline to match current route count.
This commit is contained in:
+2
-2
@@ -7,7 +7,7 @@ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/provide
|
||||
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
import { validateTriggerPaste } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
|
||||
import { useExecutionStore, useIsCurrentWorkflowExecuting } from '@/stores/execution'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
@@ -114,7 +114,7 @@ export const ActionBar = memo(
|
||||
)
|
||||
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const { isExecuting } = useCurrentWorkflowExecution()
|
||||
const isExecuting = useIsCurrentWorkflowExecuting()
|
||||
const getLastExecutionSnapshot = useExecutionStore((s) => s.getLastExecutionSnapshot)
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
const edges = useWorkflowStore((state) => state.edges)
|
||||
|
||||
@@ -55,7 +55,7 @@ import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowI
|
||||
import type { BlockLog, ExecutionResult } from '@/executor/types'
|
||||
import { useChatStore } from '@/stores/chat/store'
|
||||
import { getChatPosition } from '@/stores/chat/utils'
|
||||
import { useCurrentWorkflowExecution } from '@/stores/execution'
|
||||
import { useIsCurrentWorkflowExecuting } from '@/stores/execution'
|
||||
import { useOperationQueue } from '@/stores/operation-queue/store'
|
||||
import { useTerminalConsoleStore, useWorkflowConsoleEntries } from '@/stores/terminal'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -269,7 +269,7 @@ export function Chat() {
|
||||
const entries = useWorkflowConsoleEntries(
|
||||
hasConsoleHydrated && typeof activeWorkflowId === 'string' ? activeWorkflowId : undefined
|
||||
)
|
||||
const { isExecuting } = useCurrentWorkflowExecution()
|
||||
const isExecuting = useIsCurrentWorkflowExecuting()
|
||||
const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution()
|
||||
const { data: session } = useSession()
|
||||
const { addToQueue } = useOperationQueue()
|
||||
|
||||
+5
-14
@@ -12,21 +12,12 @@ const NOTIFICATION_WIDTH = 240
|
||||
const NOTIFICATION_GAP = 16
|
||||
|
||||
export const DiffControls = memo(function DiffControls() {
|
||||
const { isDiffReady, hasActiveDiff, acceptChanges, rejectChanges } = useWorkflowDiffStore(
|
||||
useCallback(
|
||||
(state) => ({
|
||||
isDiffReady: state.isDiffReady,
|
||||
hasActiveDiff: state.hasActiveDiff,
|
||||
acceptChanges: state.acceptChanges,
|
||||
rejectChanges: state.rejectChanges,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
)
|
||||
const isDiffReady = useWorkflowDiffStore((state) => state.isDiffReady)
|
||||
const hasActiveDiff = useWorkflowDiffStore((state) => state.hasActiveDiff)
|
||||
const acceptChanges = useWorkflowDiffStore((state) => state.acceptChanges)
|
||||
const rejectChanges = useWorkflowDiffStore((state) => state.rejectChanges)
|
||||
|
||||
const { activeWorkflowId } = useWorkflowRegistry(
|
||||
useCallback((state) => ({ activeWorkflowId: state.activeWorkflowId }), [])
|
||||
)
|
||||
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
|
||||
const allNotifications = useNotificationStore((state) => state.notifications)
|
||||
const hasVisibleNotifications = useMemo(() => {
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ import { useDeploymentInfo, useUpdatePublicApi } from '@/hooks/queries/deploymen
|
||||
import { useUpdateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { EMPTY_SUBBLOCK_VALUES, useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
type NormalizedField = InputFormatField & { name: string }
|
||||
@@ -38,8 +38,8 @@ export function ApiInfoModal({ open, onOpenChange, workflowId }: ApiInfoModalPro
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>()
|
||||
const blocks = useWorkflowStore((state) => state.blocks)
|
||||
const setValue = useSubBlockStore((state) => state.setValue)
|
||||
const subBlockValues = useSubBlockStore((state) =>
|
||||
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
|
||||
const subBlockValues = useSubBlockStore(
|
||||
(state) => (workflowId ? state.workflowValues[workflowId] : undefined) ?? EMPTY_SUBBLOCK_VALUES
|
||||
)
|
||||
|
||||
const { data: workflows = {} } = useWorkflowMap(workspaceId)
|
||||
|
||||
+3
-3
@@ -27,7 +27,7 @@ import {
|
||||
type WorkflowMcpServer,
|
||||
type WorkflowMcpTool,
|
||||
} from '@/hooks/queries/workflow-mcp-servers'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { EMPTY_SUBBLOCK_VALUES, useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('McpToolDeploy')
|
||||
@@ -120,8 +120,8 @@ export function McpDeploy({
|
||||
return null
|
||||
}, [blocks])
|
||||
|
||||
const subBlockValues = useSubBlockStore((state) =>
|
||||
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
|
||||
const subBlockValues = useSubBlockStore(
|
||||
(state) => (workflowId ? state.workflowValues[workflowId] : undefined) ?? EMPTY_SUBBLOCK_VALUES
|
||||
)
|
||||
|
||||
const inputFormat = useMemo((): NormalizedField[] => {
|
||||
|
||||
+3
-3
@@ -33,7 +33,7 @@ import { normalizeName } from '@/executor/constants'
|
||||
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 { EMPTY_SUBBLOCK_VALUES, useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
@@ -980,8 +980,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
return new Set<string>(rawAccessiblePrefixes)
|
||||
}, [rawAccessiblePrefixes])
|
||||
|
||||
const workflowSubBlockValues = useSubBlockStore((state) =>
|
||||
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
|
||||
const workflowSubBlockValues = useSubBlockStore(
|
||||
(state) => (workflowId ? state.workflowValues[workflowId] : undefined) ?? EMPTY_SUBBLOCK_VALUES
|
||||
)
|
||||
|
||||
const getMergedSubBlocks = useCallback(
|
||||
|
||||
+14
-1
@@ -682,6 +682,7 @@ export const StructuredOutput = memo(function StructuredOutput({
|
||||
computeInitialPaths(data, isError)
|
||||
)
|
||||
const prevDataRef = useRef(data)
|
||||
const prevDataJsonRef = useRef<string | null>(null)
|
||||
const prevIsErrorRef = useRef(isError)
|
||||
const internalRef = useRef<HTMLDivElement>(null)
|
||||
const listRef = useListRef(null)
|
||||
@@ -712,10 +713,22 @@ export const StructuredOutput = memo(function StructuredOutput({
|
||||
|
||||
// Reset expanded paths when data changes
|
||||
useEffect(() => {
|
||||
if (prevDataRef.current !== data || prevIsErrorRef.current !== isError) {
|
||||
if (prevIsErrorRef.current !== isError) {
|
||||
prevDataRef.current = data
|
||||
prevIsErrorRef.current = isError
|
||||
prevDataJsonRef.current = JSON.stringify(data)
|
||||
setExpandedPaths(computeInitialPaths(data, isError))
|
||||
return
|
||||
}
|
||||
|
||||
if (prevDataRef.current !== data) {
|
||||
const newJson = JSON.stringify(data)
|
||||
const prevJson = prevDataJsonRef.current ?? JSON.stringify(prevDataRef.current)
|
||||
if (prevJson !== newJson) {
|
||||
setExpandedPaths(computeInitialPaths(data, isError))
|
||||
}
|
||||
prevDataJsonRef.current = newJson
|
||||
prevDataRef.current = data
|
||||
}
|
||||
}, [data, isError])
|
||||
|
||||
|
||||
@@ -366,14 +366,23 @@ export function buildEntryTree(entries: ConsoleEntry[], idPrefix = ''): EntryNod
|
||||
}
|
||||
}
|
||||
|
||||
const nestedByContainerId = new Map<string, ConsoleEntry[]>()
|
||||
for (const e of nestedIterationEntries) {
|
||||
const parent = e.parentIterations?.[0]
|
||||
if (!parent) continue
|
||||
const list = nestedByContainerId.get(parent.iterationContainerId)
|
||||
if (list) {
|
||||
list.push(e)
|
||||
} else {
|
||||
nestedByContainerId.set(parent.iterationContainerId, [e])
|
||||
}
|
||||
}
|
||||
|
||||
const subflowNodes: EntryNode[] = []
|
||||
for (const subflowGroup of subflowGroups.values()) {
|
||||
const { iterationType, iterationContainerId, groups: iterationGroups } = subflowGroup
|
||||
|
||||
const nestedForThisSubflow = nestedIterationEntries.filter((e) => {
|
||||
const parent = e.parentIterations?.[0]
|
||||
return parent && parent.iterationContainerId === iterationContainerId
|
||||
})
|
||||
const nestedForThisSubflow = nestedByContainerId.get(iterationContainerId) ?? []
|
||||
|
||||
const allDirectBlocks = iterationGroups.flatMap((g) => g.blocks)
|
||||
const allRelevantBlocks = [...allDirectBlocks, ...nestedForThisSubflow]
|
||||
@@ -406,12 +415,21 @@ export function buildEntryTree(entries: ConsoleEntry[], idPrefix = ''): EntryNod
|
||||
iterationContainerId,
|
||||
}
|
||||
|
||||
const nestedByIteration = new Map<number, ConsoleEntry[]>()
|
||||
for (const e of nestedForThisSubflow) {
|
||||
const iterNum = e.parentIterations?.[0]?.iterationCurrent
|
||||
if (iterNum === undefined) continue
|
||||
const list = nestedByIteration.get(iterNum)
|
||||
if (list) {
|
||||
list.push(e)
|
||||
} else {
|
||||
nestedByIteration.set(iterNum, [e])
|
||||
}
|
||||
}
|
||||
|
||||
const iterationNodes: EntryNode[] = iterationGroups
|
||||
.map((iterGroup): EntryNode | null => {
|
||||
const matchingNestedEntries = nestedForThisSubflow.filter((e) => {
|
||||
const parent = e.parentIterations?.[0]
|
||||
return parent?.iterationCurrent === iterGroup.iterationCurrent
|
||||
})
|
||||
const matchingNestedEntries = nestedByIteration.get(iterGroup.iterationCurrent) ?? []
|
||||
|
||||
const strippedNestedEntries: ConsoleEntry[] = matchingNestedEntries.map((e) => ({
|
||||
...e,
|
||||
|
||||
+2
-10
@@ -1,4 +1,3 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { DiffStatus } from '@/lib/workflows/diff/types'
|
||||
import { hasDiffStatus } from '@/lib/workflows/diff/types'
|
||||
import { useIsBlockActive } from '@/stores/execution'
|
||||
@@ -54,15 +53,8 @@ export function useBlockState(
|
||||
: undefined
|
||||
|
||||
// Get diff-related data
|
||||
const { diffAnalysis, isShowingDiff } = useWorkflowDiffStore(
|
||||
useCallback(
|
||||
(state) => ({
|
||||
diffAnalysis: state.diffAnalysis,
|
||||
isShowingDiff: state.isShowingDiff,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
)
|
||||
const diffAnalysis = useWorkflowDiffStore((state) => state.diffAnalysis)
|
||||
const isShowingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
|
||||
|
||||
const isDeletedBlock = !isShowingDiff && diffAnalysis?.deleted_blocks?.includes(blockId)
|
||||
|
||||
|
||||
+15
-3
@@ -38,7 +38,7 @@ import { subscriptionKeys } from '@/hooks/queries/subscription'
|
||||
import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
|
||||
import { isExecutionStreamHttpError, useExecutionStream } from '@/hooks/use-execution-stream'
|
||||
import { WorkflowValidationError } from '@/serializer'
|
||||
import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/execution'
|
||||
import { defaultWorkflowExecutionState, useExecutionStore } from '@/stores/execution'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import {
|
||||
clearExecutionPointer,
|
||||
@@ -136,8 +136,20 @@ export function useWorkflowExecution() {
|
||||
variables: s.variables,
|
||||
}))
|
||||
)
|
||||
const { isExecuting, isDebugging, pendingBlocks, executor, debugContext } =
|
||||
useCurrentWorkflowExecution()
|
||||
const { isExecuting, isDebugging, pendingBlocks, executor, debugContext } = useExecutionStore(
|
||||
useShallow((state) => {
|
||||
const exec = activeWorkflowId
|
||||
? (state.workflowExecutions.get(activeWorkflowId) ?? defaultWorkflowExecutionState)
|
||||
: defaultWorkflowExecutionState
|
||||
return {
|
||||
isExecuting: exec.isExecuting,
|
||||
isDebugging: exec.isDebugging,
|
||||
pendingBlocks: exec.pendingBlocks,
|
||||
executor: exec.executor,
|
||||
debugContext: exec.debugContext,
|
||||
}
|
||||
})
|
||||
)
|
||||
const setCurrentExecutionId = useExecutionStore((s) => s.setCurrentExecutionId)
|
||||
const getCurrentExecutionId = useExecutionStore((s) => s.getCurrentExecutionId)
|
||||
const rawSetIsExecuting = useExecutionStore((s) => s.setIsExecuting)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { EdgeTypes, NodeTypes } from 'reactflow'
|
||||
import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
|
||||
import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block'
|
||||
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
||||
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
|
||||
|
||||
/** Custom node types for ReactFlow. */
|
||||
export const nodeTypes: NodeTypes = {
|
||||
workflowBlock: WorkflowBlock,
|
||||
noteBlock: NoteBlock,
|
||||
subflowNode: SubflowNodeComponent,
|
||||
}
|
||||
|
||||
/** Custom edge types for ReactFlow. */
|
||||
export const edgeTypes: EdgeTypes = {
|
||||
default: WorkflowEdge,
|
||||
workflowEdge: WorkflowEdge,
|
||||
}
|
||||
|
||||
/** ReactFlow configuration constants. */
|
||||
export const defaultEdgeOptions = { type: 'custom' } as const
|
||||
|
||||
export const reactFlowStyles = [
|
||||
'[&_.react-flow__handle]:!z-[30]',
|
||||
'[&_.react-flow__edge-labels]:!z-[1001]',
|
||||
'[&_.react-flow__pane]:select-none',
|
||||
'[&_.react-flow__selectionpane]:select-none',
|
||||
'[&_.react-flow__background]:hidden',
|
||||
'[&_.react-flow__node-subflowNode.selected]:!shadow-none',
|
||||
].join(' ')
|
||||
|
||||
export const reactFlowFitViewOptions = { padding: 0.6, maxZoom: 1.0 } as const
|
||||
export const embeddedFitViewOptions = { padding: 0.15, maxZoom: 0.85, minZoom: 0.1 } as const
|
||||
export const embeddedResizeFitViewOptions = { ...embeddedFitViewOptions, duration: 0 } as const
|
||||
export const reactFlowProOptions = { hideAttribution: true } as const
|
||||
@@ -6,10 +6,8 @@ import ReactFlow, {
|
||||
applyNodeChanges,
|
||||
ConnectionLineType,
|
||||
type Edge,
|
||||
type EdgeTypes,
|
||||
type Node,
|
||||
type NodeChange,
|
||||
type NodeTypes,
|
||||
ReactFlowProvider,
|
||||
SelectionMode,
|
||||
useReactFlow,
|
||||
@@ -31,18 +29,14 @@ import {
|
||||
DiffControls,
|
||||
Notifications,
|
||||
Panel,
|
||||
SubflowNodeComponent,
|
||||
Terminal,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
|
||||
import { BlockMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu'
|
||||
import { CanvasMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu'
|
||||
import { Cursors } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors'
|
||||
import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index'
|
||||
import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block'
|
||||
import type { SubflowNodeData } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
|
||||
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
||||
import { WorkflowControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls'
|
||||
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
|
||||
import {
|
||||
useAutoLayout,
|
||||
useCanvasContextMenu,
|
||||
@@ -70,6 +64,16 @@ import {
|
||||
resolveSelectionConflicts,
|
||||
validateTriggerPaste,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
|
||||
import {
|
||||
defaultEdgeOptions,
|
||||
edgeTypes,
|
||||
embeddedFitViewOptions,
|
||||
embeddedResizeFitViewOptions,
|
||||
nodeTypes,
|
||||
reactFlowFitViewOptions,
|
||||
reactFlowProOptions,
|
||||
reactFlowStyles,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/workflow-constants'
|
||||
import { useSocket } from '@/app/workspace/providers/socket-provider'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { isAnnotationOnlyBlock } from '@/executor/constants'
|
||||
@@ -180,35 +184,6 @@ function syncPanelWithSelection(selectedIds: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Custom node types for ReactFlow. */
|
||||
const nodeTypes: NodeTypes = {
|
||||
workflowBlock: WorkflowBlock,
|
||||
noteBlock: NoteBlock,
|
||||
subflowNode: SubflowNodeComponent,
|
||||
}
|
||||
|
||||
/** Custom edge types for ReactFlow. */
|
||||
const edgeTypes: EdgeTypes = {
|
||||
default: WorkflowEdge,
|
||||
workflowEdge: WorkflowEdge,
|
||||
}
|
||||
|
||||
/** ReactFlow configuration constants. */
|
||||
const defaultEdgeOptions = { type: 'custom' }
|
||||
|
||||
const reactFlowStyles = [
|
||||
'[&_.react-flow__handle]:!z-[30]',
|
||||
'[&_.react-flow__edge-labels]:!z-[1001]',
|
||||
'[&_.react-flow__pane]:select-none',
|
||||
'[&_.react-flow__selectionpane]:select-none',
|
||||
'[&_.react-flow__background]:hidden',
|
||||
'[&_.react-flow__node-subflowNode.selected]:!shadow-none',
|
||||
].join(' ')
|
||||
const reactFlowFitViewOptions = { padding: 0.6, maxZoom: 1.0 } as const
|
||||
const embeddedFitViewOptions = { padding: 0.15, maxZoom: 0.85, minZoom: 0.1 } as const
|
||||
const embeddedResizeFitViewOptions = { ...embeddedFitViewOptions, duration: 0 } as const
|
||||
const reactFlowProOptions = { hideAttribution: true } as const
|
||||
|
||||
/**
|
||||
* Map from edge contextId to edge id.
|
||||
* Context IDs include parent loop info for edges inside loops.
|
||||
|
||||
@@ -2,6 +2,7 @@ export {
|
||||
useCurrentWorkflowExecution,
|
||||
useExecutionStore,
|
||||
useIsBlockActive,
|
||||
useIsCurrentWorkflowExecuting,
|
||||
useLastRunEdges,
|
||||
useLastRunPath,
|
||||
} from './store'
|
||||
|
||||
@@ -180,6 +180,20 @@ export function useCurrentWorkflowExecution(): WorkflowExecutionState {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the active workflow is currently executing.
|
||||
* More granular than useCurrentWorkflowExecution — only re-renders when
|
||||
* the isExecuting boolean changes, not on every iteration update during
|
||||
* parallel-loop runs.
|
||||
*/
|
||||
export function useIsCurrentWorkflowExecuting(): boolean {
|
||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||
return useExecutionStore((state) => {
|
||||
if (!activeWorkflowId) return false
|
||||
return state.workflowExecutions.get(activeWorkflowId)?.isExecuting ?? false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a specific block is currently active (executing) in the current workflow.
|
||||
* More granular than useCurrentWorkflowExecution — only re-renders when
|
||||
|
||||
@@ -6,11 +6,18 @@ import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { populateTriggerFieldsFromConfig } from '@/hooks/use-trigger-config-aggregation'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import type { SubBlockStore } from '@/stores/workflows/subblock/types'
|
||||
import type { SubBlockStore, SubBlockValue } from '@/stores/workflows/subblock/types'
|
||||
import { isTriggerValid } from '@/triggers'
|
||||
|
||||
const logger = createLogger('SubBlockStore')
|
||||
|
||||
/**
|
||||
* Stable empty fallback for `state.workflowValues[workflowId]` selectors.
|
||||
* Using a module-level constant avoids returning a fresh `{}` on every
|
||||
* selector call, which would defeat Zustand's `Object.is` equality.
|
||||
*/
|
||||
export const EMPTY_SUBBLOCK_VALUES: Record<string, Record<string, SubBlockValue>> = {}
|
||||
|
||||
/**
|
||||
* SubBlockState stores values for all subblocks in workflows
|
||||
*
|
||||
|
||||
@@ -71,6 +71,7 @@ vi.mock('@/stores/execution/store', () => ({
|
||||
lastRunEdges: new Map(),
|
||||
}),
|
||||
useIsBlockActive: vi.fn().mockReturnValue(false),
|
||||
useIsCurrentWorkflowExecuting: vi.fn().mockReturnValue(false),
|
||||
useLastRunPath: vi.fn().mockReturnValue(new Map()),
|
||||
useLastRunEdges: vi.fn().mockReturnValue(new Map()),
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user