Files
simstudioai--sim/apps/sim/stores/workflows/subblock/store.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

170 lines
5.8 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
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, 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>> = {}
/**
* Stable empty fallback for a single block's sub-block values.
*/
export const EMPTY_BLOCK_SUBBLOCK_VALUES: Record<string, SubBlockValue> = {}
/**
* SubBlockState stores values for all subblocks in workflows
*
* Important implementation notes:
* 1. Values are stored per workflow, per block, per subblock
* 2. When workflows are synced to the database, the mergeSubblockState function
* in utils.ts combines the block structure with these values
* 3. If a subblock value exists here but not in the block structure
* (e.g., inputFormat in starter block), the merge function will include it
* in the synchronized state to ensure persistence
*/
export const useSubBlockStore = create<SubBlockStore>()(
devtools((set, get) => ({
workflowValues: {},
setValue: (blockId: string, subBlockId: string, value: any) => {
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return
let validatedValue = value
if (Array.isArray(value)) {
const isTableData =
value.length > 0 &&
value.some((item) => item && typeof item === 'object' && 'cells' in item)
if (isTableData) {
logger.debug('Validating table data for subblock', { blockId, subBlockId })
validatedValue = value.map((row: any) => {
if (!row || typeof row !== 'object') {
logger.warn('Fixing malformed table row', { blockId, subBlockId, row })
return {
id: generateId(),
cells: { Key: '', Value: '' },
}
}
if (!row.id) {
row.id = generateId()
}
if (!row.cells || typeof row.cells !== 'object') {
logger.warn('Fixing malformed table row cells', { blockId, subBlockId, row })
row.cells = { Key: '', Value: '' }
}
return row
})
}
}
set((state) => ({
workflowValues: {
...state.workflowValues,
[activeWorkflowId]: {
...state.workflowValues[activeWorkflowId],
[blockId]: {
...state.workflowValues[activeWorkflowId]?.[blockId],
[subBlockId]: validatedValue,
},
},
},
}))
},
getValue: (blockId: string, subBlockId: string) => {
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return null
return get().workflowValues[activeWorkflowId]?.[blockId]?.[subBlockId] ?? null
},
clear: () => {
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return
set((state) => ({
workflowValues: {
...state.workflowValues,
[activeWorkflowId]: {},
},
}))
},
initializeFromWorkflow: (workflowId: string, blocks: Record<string, any>) => {
const values: Record<string, Record<string, any>> = {}
Object.entries(blocks).forEach(([blockId, block]) => {
values[blockId] = {}
Object.entries(block.subBlocks || {}).forEach(([subBlockId, subBlock]) => {
values[blockId][subBlockId] = (subBlock as SubBlockConfig).value
})
})
set((state) => ({
workflowValues: {
...state.workflowValues,
[workflowId]: values,
},
}))
Object.entries(blocks).forEach(([blockId, block]) => {
const blockConfig = getBlock(block.type)
if (!blockConfig) return
const isTriggerBlock = blockConfig.category === 'triggers' || block.triggerMode === true
if (!isTriggerBlock) return
let triggerId: string | undefined
if (blockConfig.category === 'triggers') {
triggerId = block.type
} else if (block.triggerMode === true && blockConfig.triggers?.enabled) {
const selectedTriggerIdValue = block.subBlocks?.selectedTriggerId?.value
const triggerIdValue = block.subBlocks?.triggerId?.value
triggerId =
(typeof selectedTriggerIdValue === 'string' && isTriggerValid(selectedTriggerIdValue)
? selectedTriggerIdValue
: undefined) ||
(typeof triggerIdValue === 'string' && isTriggerValid(triggerIdValue)
? triggerIdValue
: undefined) ||
blockConfig.triggers?.available?.[0]
}
if (!triggerId || !isTriggerValid(triggerId)) {
return
}
const triggerConfigSubBlock = block.subBlocks?.triggerConfig
if (triggerConfigSubBlock?.value && typeof triggerConfigSubBlock.value === 'object') {
populateTriggerFieldsFromConfig(blockId, triggerConfigSubBlock.value, triggerId)
}
})
},
setWorkflowValues: (workflowId: string, values: Record<string, Record<string, any>>) => {
set((state) => ({
workflowValues: {
...state.workflowValues,
[workflowId]: values,
},
}))
},
}))
)