chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+546
View File
@@ -0,0 +1,546 @@
/**
* Pure display helpers for collapsed subblock rows.
*
* Shared by the canvas editor (`workflow-block.tsx`, hook-fed data) and the
* read-only preview (`preview-workflow/.../block.tsx`, prop/store-fed data).
* Every resolver takes plain data instead of hooks so both surfaces run the
* exact same logic and cannot drift.
*/
import { isRecordLike } from '@sim/utils/object'
import { truncate } from '@sim/utils/string'
import type { FilterRule, SortRule } from '@/lib/table/types'
import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
/**
* Joins display names as "A", "A, B", or "A, B +N".
* Returns null for an empty list so callers can fall through.
*/
export function summarizeNames(names: string[]): string | null {
if (names.length === 0) return null
if (names.length === 1) return names[0]
if (names.length === 2) return `${names[0]}, ${names[1]}`
return `${names[0]}, ${names[1]} +${names.length - 2}`
}
interface WorkflowTableRow {
id: string
cells: Record<string, string>
}
interface FieldFormat {
id: string
name: string
type?: string
value?: string
collapsed?: boolean
}
interface TagFilterItem {
id: string
tagName: string
fieldType?: string
operator?: string
tagValue: string
}
interface DocumentTagItem {
id: string
tagName: string
fieldType?: string
value: string
}
const isTableRowArray = (value: unknown): value is WorkflowTableRow[] => {
if (!Array.isArray(value) || value.length === 0) return false
const firstItem = value[0]
return (
typeof firstItem === 'object' &&
firstItem !== null &&
'id' in firstItem &&
'cells' in firstItem &&
typeof firstItem.cells === 'object'
)
}
const isFieldFormatArray = (value: unknown): value is FieldFormat[] => {
if (!Array.isArray(value) || value.length === 0) return false
const firstItem = value[0]
return (
typeof firstItem === 'object' &&
firstItem !== null &&
'id' in firstItem &&
'name' in firstItem &&
typeof firstItem.name === 'string'
)
}
/** Type guard for variable assignments arrays (variables-input subblocks). */
const isVariableAssignmentsArray = (
value: unknown
): value is Array<{ id?: string; variableId?: string; variableName?: string; value: unknown }> => {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every(
(item) =>
typeof item === 'object' &&
item !== null &&
('variableName' in item || 'variableId' in item)
)
)
}
const isMessagesArray = (value: unknown): value is Array<{ role: string; content: string }> => {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every(
(item) =>
typeof item === 'object' &&
item !== null &&
'role' in item &&
'content' in item &&
typeof item.role === 'string' &&
typeof item.content === 'string'
)
)
}
const isTagFilterArray = (value: unknown): value is TagFilterItem[] => {
if (!Array.isArray(value) || value.length === 0) return false
const firstItem = value[0]
return (
typeof firstItem === 'object' &&
firstItem !== null &&
'tagName' in firstItem &&
'tagValue' in firstItem &&
typeof firstItem.tagName === 'string'
)
}
const isDocumentTagArray = (value: unknown): value is DocumentTagItem[] => {
if (!Array.isArray(value) || value.length === 0) return false
const firstItem = value[0]
return (
typeof firstItem === 'object' &&
firstItem !== null &&
'tagName' in firstItem &&
'value' in firstItem &&
!('tagValue' in firstItem) &&
typeof firstItem.tagName === 'string'
)
}
const isFilterConditionArray = (value: unknown): value is FilterRule[] => {
if (!Array.isArray(value) || value.length === 0) return false
const firstItem = value[0]
return (
typeof firstItem === 'object' &&
firstItem !== null &&
'column' in firstItem &&
'operator' in firstItem &&
'logicalOperator' in firstItem &&
typeof firstItem.column === 'string'
)
}
const isSortConditionArray = (value: unknown): value is SortRule[] => {
if (!Array.isArray(value) || value.length === 0) return false
const firstItem = value[0]
return (
typeof firstItem === 'object' &&
firstItem !== null &&
'column' in firstItem &&
'direction' in firstItem &&
typeof firstItem.column === 'string' &&
(firstItem.direction === 'asc' || firstItem.direction === 'desc')
)
}
/**
* Attempts to parse a JSON string, returning the parsed value or the
* original value if parsing fails.
*/
const tryParseJson = (value: unknown): unknown => {
if (typeof value !== 'string') return value
try {
const trimmed = value.trim()
if (
(trimmed.startsWith('[') && trimmed.endsWith(']')) ||
(trimmed.startsWith('{') && trimmed.endsWith('}'))
) {
return JSON.parse(trimmed)
}
} catch {
return value
}
return value
}
/**
* Formats a subblock value for display, intelligently handling nested
* objects and arrays.
*/
export const getDisplayValue = (value: unknown): string => {
if (value == null || value === '') return '-'
const parsedValue = tryParseJson(value)
if (isMessagesArray(parsedValue)) {
const firstMessage = parsedValue[0]
if (!firstMessage?.content || firstMessage.content.trim() === '') return '-'
const content = firstMessage.content.trim()
return truncate(content, 50)
}
if (isVariableAssignmentsArray(parsedValue)) {
const names = parsedValue.map((a) => a.variableName).filter((name): name is string => !!name)
return summarizeNames(names) ?? '-'
}
if (isTagFilterArray(parsedValue)) {
const names = parsedValue
.filter((f) => typeof f.tagName === 'string' && f.tagName.trim() !== '')
.map((f) => f.tagName)
return summarizeNames(names) ?? '-'
}
if (isDocumentTagArray(parsedValue)) {
const names = parsedValue
.filter((t) => typeof t.tagName === 'string' && t.tagName.trim() !== '')
.map((t) => t.tagName)
return summarizeNames(names) ?? '-'
}
if (isFilterConditionArray(parsedValue)) {
const opLabels: Record<string, string> = {
eq: '=',
ne: '≠',
gt: '>',
gte: '≥',
lt: '<',
lte: '≤',
contains: '~',
in: 'in',
}
const names = parsedValue
.filter((c) => typeof c.column === 'string' && c.column.trim() !== '')
.map((c) => `${c.column} ${opLabels[c.operator] || c.operator} ${c.value || '?'}`)
return summarizeNames(names) ?? '-'
}
if (isSortConditionArray(parsedValue)) {
const names = parsedValue
.filter((c) => typeof c.column === 'string' && c.column.trim() !== '')
.map((c) => `${c.column} ${c.direction === 'desc' ? '↓' : '↑'}`)
return summarizeNames(names) ?? '-'
}
if (isTableRowArray(parsedValue)) {
const nonEmptyRows = parsedValue.filter((row) => {
const cellValues = Object.values(row.cells)
return cellValues.some((cell) => cell && cell.trim() !== '')
})
if (nonEmptyRows.length === 0) return '-'
if (nonEmptyRows.length === 1) {
const firstRow = nonEmptyRows[0]
const cellEntries = Object.entries(firstRow.cells).filter(([, val]) => val?.trim())
if (cellEntries.length === 0) return '-'
const preview = cellEntries
.slice(0, 2)
.map(([key, val]) => `${key}: ${val}`)
.join(', ')
return cellEntries.length > 2 ? `${preview}...` : preview
}
return `${nonEmptyRows.length} rows`
}
if (isFieldFormatArray(parsedValue)) {
const names = parsedValue
.filter((field) => typeof field.name === 'string' && field.name.trim() !== '')
.map((field) => field.name)
return summarizeNames(names) ?? '-'
}
if (isRecordLike(parsedValue)) {
const entries = Object.entries(parsedValue).filter(
([, val]) => val !== null && val !== undefined && val !== ''
)
if (entries.length === 0) return '-'
if (entries.length === 1) {
const [key, val] = entries[0]
const valStr = String(val).slice(0, 30)
return `${key}: ${valStr}${String(val).length > 30 ? '...' : ''}`
}
const preview = entries
.slice(0, 2)
.map(([key]) => key)
.join(', ')
return entries.length > 2 ? `${preview} +${entries.length - 2}` : preview
}
if (Array.isArray(parsedValue)) {
const getItemDisplayValue = (item: unknown): string => {
if (typeof item === 'object' && item !== null) {
const obj = item as Record<string, unknown>
return String(obj.title || obj.name || obj.label || obj.id || JSON.stringify(item))
}
return String(item)
}
const names = parsedValue
.filter((item) => item !== null && item !== undefined && item !== '')
.map(getItemDisplayValue)
return summarizeNames(names) ?? '-'
}
const stringValue = String(value)
if (stringValue === '[object Object]') {
try {
const json = JSON.stringify(parsedValue)
if (json.length <= 40) return json
return truncate(json, 37)
} catch {
return '-'
}
}
return stringValue.trim().length > 0 ? stringValue : '-'
}
/**
* Workflow id -> metadata lookup for the workflow selector resolvers.
* `ready` gates resolution so missing entries only render as deleted once
* the lookup has actually loaded.
*/
interface WorkflowNameLookup {
workflowMap: Record<string, { name: string }>
ready: boolean
}
/**
* Resolves filter/sort builder subblocks to a compact single-line JSON
* preview. Returns null for other subblocks; callers use a non-null result
* to apply monospace styling.
*/
export function resolveFilterFieldLabel(
subBlock: SubBlockConfig | undefined,
rawValue: unknown
): string | null {
const isFilterField =
subBlock?.id === 'filter' || subBlock?.id === 'filterCriteria' || subBlock?.id === 'sort'
if (!isFilterField || !rawValue) return null
const parsedValue = tryParseJson(rawValue)
if (!isRecordLike(parsedValue) && !Array.isArray(parsedValue)) return null
try {
const jsonStr = JSON.stringify(parsedValue, null, 0)
return jsonStr.length <= 35 ? jsonStr : truncate(jsonStr, 32)
} catch {
return null
}
}
/**
* Resolves a static dropdown/combobox value to its option label.
* Returns null if not a dropdown/combobox or no matching option is found.
*/
export function resolveDropdownLabel(
subBlock: SubBlockConfig | undefined,
rawValue: unknown
): string | null {
if (!subBlock || (subBlock.type !== 'dropdown' && subBlock.type !== 'combobox')) return null
if (!rawValue || typeof rawValue !== 'string') return null
const options = typeof subBlock.options === 'function' ? subBlock.options() : subBlock.options
if (!options) return null
const option = options.find((opt) =>
typeof opt === 'string' ? opt === rawValue : opt.id === rawValue
)
if (!option) return null
return typeof option === 'string' ? option : option.label
}
/** Resolves a workflow-selector value to the workflow's name. */
export function resolveWorkflowSelectionLabel(
subBlock: SubBlockConfig | undefined,
rawValue: unknown,
lookup: WorkflowNameLookup
): string | null {
if (subBlock?.type !== 'workflow-selector') return null
if (!rawValue || typeof rawValue !== 'string') return null
if (!lookup.ready) return null
return lookup.workflowMap[rawValue]?.name ?? DELETED_WORKFLOW_LABEL
}
/**
* Resolves multi-select workflow dropdowns (e.g. the Sim trigger's workflow
* scope) to a workflow-name summary.
*/
export function resolveWorkflowMultiSelectLabel(
subBlock: SubBlockConfig | undefined,
rawValue: unknown,
lookup: WorkflowNameLookup
): string | null {
const isWorkflowMultiSelect =
subBlock?.type === 'dropdown' &&
subBlock.multiSelect &&
(subBlock.id === 'workflowIds' || subBlock.canonicalParamId === 'workflowIds')
if (!isWorkflowMultiSelect) return null
if (!Array.isArray(rawValue) || rawValue.length === 0) return null
if (!lookup.ready) return null
const names = rawValue
.filter((id): id is string => typeof id === 'string' && id.length > 0)
.map((id) => lookup.workflowMap[id]?.name ?? DELETED_WORKFLOW_LABEL)
return summarizeNames(names)
}
/** Resolves a variables-input value to a variable-name summary. */
export function resolveVariablesLabel(
subBlock: SubBlockConfig | undefined,
rawValue: unknown,
variables: Array<{ id: string; name: string }>
): string | null {
if (subBlock?.type !== 'variables-input') return null
if (!isVariableAssignmentsArray(rawValue)) return null
const names = rawValue
.map((assignment) => {
if (assignment.variableId) {
return variables.find((variable) => variable.id === assignment.variableId)?.name
}
if (assignment.variableName) return assignment.variableName
return null
})
.filter((name): name is string => !!name)
return summarizeNames(names)
}
/** Custom-tool record shape needed to resolve a stored custom-tool reference. */
export interface StoredCustomToolRecord {
id: string
title?: string
schema?: { function?: { name?: string } }
}
export interface ResolveStoredToolNameOptions {
customTools?: StoredCustomToolRecord[]
/** Live MCP tool names keyed by composite tool id (`createMcpToolId`). */
mcpToolNamesById?: ReadonlyMap<string, string>
/** Block-config lookup; overridable so snapshot views can scope configs. */
getBlockConfig?: (type: string) => { name?: string } | undefined
}
/**
* Resolves one stored tool entry to its display name. Canonical sources win —
* the block registry (including the custom-block overlay), the custom-tool
* record, live MCP server data — so edits to the entry's mutable `title` in
* workflow state cannot relabel a tool that has a canonical name. The stored
* title is the fallback for entries with no resolvable source (deleted custom
* blocks, MCP entries while server data is unavailable, legacy inline custom
* tools where the title is the identity), ahead of the raw type id.
*/
export function resolveStoredToolName(
tool: unknown,
options: ResolveStoredToolNameOptions = {}
): string | null {
if (!tool || typeof tool !== 'object') return null
const t = tool as Record<string, unknown>
const { customTools = [], mcpToolNamesById, getBlockConfig = getBlock } = options
const storedTitle = typeof t.title === 'string' && t.title ? t.title : null
const schema = t.schema as { function?: { name?: string } } | undefined
const schemaName = schema?.function?.name || null
if (t.type === 'custom-tool') {
if (typeof t.customToolId === 'string') {
const record = customTools.find((candidate) => candidate.id === t.customToolId)
if (record?.title) return record.title
if (record?.schema?.function?.name) return record.schema.function.name
}
return storedTitle || schemaName
}
if (t.type === 'mcp') {
if (typeof t.toolId === 'string') {
const liveName = mcpToolNamesById?.get(t.toolId)
if (liveName) return liveName
}
return storedTitle
}
if (typeof t.type === 'string' && t.type) {
const blockConfig = getBlockConfig(t.type)
if (blockConfig?.name) return blockConfig.name
return storedTitle || t.type
}
if (storedTitle) return storedTitle
if (schemaName) return schemaName
const fn = t.function as { name?: string } | undefined
if (fn?.name) return fn.name
return null
}
/**
* Resolves a tool-input value to a tool-name summary via
* {@link resolveStoredToolName}. Unresolvable entries are skipped.
*/
export function resolveToolsLabel(
subBlock: SubBlockConfig | undefined,
rawValue: unknown,
customTools: StoredCustomToolRecord[],
mcpToolNamesById?: ReadonlyMap<string, string>
): string | null {
if (subBlock?.type !== 'tool-input') return null
if (!Array.isArray(rawValue) || rawValue.length === 0) return null
const names = rawValue
.map((tool: unknown) => resolveStoredToolName(tool, { customTools, mcpToolNamesById }))
.filter((name): name is string => !!name)
return summarizeNames(names)
}
/**
* Resolves a skill-input value to a skill-name summary: the live skill name
* when the skill still exists, otherwise the name stored alongside the
* reference. Unresolvable entries are skipped rather than shown as raw ids.
*/
export function resolveSkillsLabel(
subBlock: SubBlockConfig | undefined,
rawValue: unknown,
skills: Array<{ id: string; name: string }>
): string | null {
if (subBlock?.type !== 'skill-input') return null
if (!Array.isArray(rawValue) || rawValue.length === 0) return null
const names = rawValue
.map((skill: unknown) => {
if (!skill || typeof skill !== 'object') return null
const s = skill as { skillId?: string; name?: string }
if (s.skillId) {
const found = skills.find((candidate) => candidate.id === s.skillId)
if (found?.name) return found.name
}
if (typeof s.name === 'string' && s.name) return s.name
return null
})
.filter((name): name is string => !!name)
return summarizeNames(names)
}