mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
v0.5.39: notion, workflow variables fixes
This commit is contained in:
@@ -37,7 +37,7 @@ import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-
|
|||||||
import type { GenerationType } from '@/blocks/types'
|
import type { GenerationType } from '@/blocks/types'
|
||||||
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
|
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
|
||||||
import { useTagSelection } from '@/hooks/use-tag-selection'
|
import { useTagSelection } from '@/hooks/use-tag-selection'
|
||||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
|
|
||||||
const logger = createLogger('Code')
|
const logger = createLogger('Code')
|
||||||
|
|
||||||
@@ -602,7 +602,7 @@ export function Code({
|
|||||||
|
|
||||||
const inner = reference.slice(1, -1)
|
const inner = reference.slice(1, -1)
|
||||||
const [prefix] = inner.split('.')
|
const [prefix] = inner.split('.')
|
||||||
const normalizedPrefix = normalizeBlockName(prefix)
|
const normalizedPrefix = normalizeName(prefix)
|
||||||
|
|
||||||
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
|
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
|
||||||
import { useTagSelection } from '@/hooks/use-tag-selection'
|
import { useTagSelection } from '@/hooks/use-tag-selection'
|
||||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
|
|
||||||
const logger = createLogger('ConditionInput')
|
const logger = createLogger('ConditionInput')
|
||||||
@@ -139,7 +139,7 @@ export function ConditionInput({
|
|||||||
|
|
||||||
const inner = reference.slice(1, -1)
|
const inner = reference.slice(1, -1)
|
||||||
const [prefix] = inner.split('.')
|
const [prefix] = inner.split('.')
|
||||||
const normalizedPrefix = normalizeBlockName(prefix)
|
const normalizedPrefix = normalizeName(prefix)
|
||||||
|
|
||||||
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react'
|
|||||||
import { splitReferenceSegment } from '@/lib/workflows/sanitization/references'
|
import { splitReferenceSegment } from '@/lib/workflows/sanitization/references'
|
||||||
import { REFERENCE } from '@/executor/constants'
|
import { REFERENCE } from '@/executor/constants'
|
||||||
import { createCombinedPattern } from '@/executor/utils/reference-validation'
|
import { createCombinedPattern } from '@/executor/utils/reference-validation'
|
||||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
|
|
||||||
export interface HighlightContext {
|
export interface HighlightContext {
|
||||||
accessiblePrefixes?: Set<string>
|
accessiblePrefixes?: Set<string>
|
||||||
@@ -31,7 +31,7 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea
|
|||||||
|
|
||||||
const inner = reference.slice(1, -1)
|
const inner = reference.slice(1, -1)
|
||||||
const [prefix] = inner.split('.')
|
const [prefix] = inner.split('.')
|
||||||
const normalizedPrefix = normalizeBlockName(prefix)
|
const normalizedPrefix = normalizeName(prefix)
|
||||||
|
|
||||||
if (SYSTEM_PREFIXES.has(normalizedPrefix)) {
|
if (SYSTEM_PREFIXES.has(normalizedPrefix)) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { useVariablesStore } from '@/stores/panel/variables/store'
|
|||||||
import type { Variable } from '@/stores/panel/variables/types'
|
import type { Variable } from '@/stores/panel/variables/types'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||||
import { getTool } from '@/tools/utils'
|
import { getTool } from '@/tools/utils'
|
||||||
@@ -117,20 +118,6 @@ const TAG_PREFIXES = {
|
|||||||
VARIABLE: 'variable.',
|
VARIABLE: 'variable.',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes a block name by removing spaces and converting to lowercase
|
|
||||||
*/
|
|
||||||
const normalizeBlockName = (blockName: string): string => {
|
|
||||||
return blockName.replace(/\s+/g, '').toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes a variable name by removing spaces
|
|
||||||
*/
|
|
||||||
const normalizeVariableName = (variableName: string): string => {
|
|
||||||
return variableName.replace(/\s+/g, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the root tag is present in the tags array
|
* Ensures the root tag is present in the tags array
|
||||||
*/
|
*/
|
||||||
@@ -521,7 +508,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
if (sourceBlock.type === 'loop' || sourceBlock.type === 'parallel') {
|
if (sourceBlock.type === 'loop' || sourceBlock.type === 'parallel') {
|
||||||
const mockConfig = { outputs: { results: 'array' } }
|
const mockConfig = { outputs: { results: 'array' } }
|
||||||
const blockName = sourceBlock.name || sourceBlock.type
|
const blockName = sourceBlock.name || sourceBlock.type
|
||||||
const normalizedBlockName = normalizeBlockName(blockName)
|
const normalizedBlockName = normalizeName(blockName)
|
||||||
|
|
||||||
const outputPaths = generateOutputPaths(mockConfig.outputs)
|
const outputPaths = generateOutputPaths(mockConfig.outputs)
|
||||||
const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||||
@@ -542,7 +529,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blockName = sourceBlock.name || sourceBlock.type
|
const blockName = sourceBlock.name || sourceBlock.type
|
||||||
const normalizedBlockName = normalizeBlockName(blockName)
|
const normalizedBlockName = normalizeName(blockName)
|
||||||
|
|
||||||
const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId)
|
const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId)
|
||||||
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
||||||
@@ -735,12 +722,12 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const variableTags = validVariables.map(
|
const variableTags = validVariables.map(
|
||||||
(variable: Variable) => `${TAG_PREFIXES.VARIABLE}${normalizeVariableName(variable.name)}`
|
(variable: Variable) => `${TAG_PREFIXES.VARIABLE}${normalizeName(variable.name)}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const variableInfoMap = validVariables.reduce(
|
const variableInfoMap = validVariables.reduce(
|
||||||
(acc, variable) => {
|
(acc, variable) => {
|
||||||
const tagName = `${TAG_PREFIXES.VARIABLE}${normalizeVariableName(variable.name)}`
|
const tagName = `${TAG_PREFIXES.VARIABLE}${normalizeName(variable.name)}`
|
||||||
acc[tagName] = {
|
acc[tagName] = {
|
||||||
type: variable.type,
|
type: variable.type,
|
||||||
id: variable.id,
|
id: variable.id,
|
||||||
@@ -865,7 +852,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
|
|
||||||
const mockConfig = { outputs: { results: 'array' } }
|
const mockConfig = { outputs: { results: 'array' } }
|
||||||
const blockName = accessibleBlock.name || accessibleBlock.type
|
const blockName = accessibleBlock.name || accessibleBlock.type
|
||||||
const normalizedBlockName = normalizeBlockName(blockName)
|
const normalizedBlockName = normalizeName(blockName)
|
||||||
|
|
||||||
const outputPaths = generateOutputPaths(mockConfig.outputs)
|
const outputPaths = generateOutputPaths(mockConfig.outputs)
|
||||||
let blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
let blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||||
@@ -885,7 +872,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blockName = accessibleBlock.name || accessibleBlock.type
|
const blockName = accessibleBlock.name || accessibleBlock.type
|
||||||
const normalizedBlockName = normalizeBlockName(blockName)
|
const normalizedBlockName = normalizeName(blockName)
|
||||||
|
|
||||||
const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId)
|
const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId)
|
||||||
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
const responseFormatValue = mergedSubBlocks?.responseFormat?.value
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/co
|
|||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
|
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
|
||||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId
|
|||||||
|
|
||||||
const inner = reference.slice(1, -1)
|
const inner = reference.slice(1, -1)
|
||||||
const [prefix] = inner.split('.')
|
const [prefix] = inner.split('.')
|
||||||
const normalizedPrefix = normalizeBlockName(prefix)
|
const normalizedPrefix = normalizeName(prefix)
|
||||||
|
|
||||||
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react'
|
|||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
|
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
|
||||||
import { SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/sanitization/references'
|
import { SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/sanitization/references'
|
||||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
|
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||||
|
|
||||||
@@ -53,10 +53,10 @@ export function useAccessibleReferencePrefixes(blockId?: string | null): Set<str
|
|||||||
|
|
||||||
const prefixes = new Set<string>()
|
const prefixes = new Set<string>()
|
||||||
accessibleIds.forEach((id) => {
|
accessibleIds.forEach((id) => {
|
||||||
prefixes.add(normalizeBlockName(id))
|
prefixes.add(normalizeName(id))
|
||||||
const block = blocks[id]
|
const block = blocks[id]
|
||||||
if (block?.name) {
|
if (block?.name) {
|
||||||
prefixes.add(normalizeBlockName(block.name))
|
prefixes.add(normalizeName(block.name))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ interface UIEnvironmentVariable {
|
|||||||
id?: number
|
id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an environment variable key.
|
||||||
|
* Returns an error message if invalid, undefined if valid.
|
||||||
|
*/
|
||||||
|
function validateEnvVarKey(key: string): string | undefined {
|
||||||
|
if (!key) return undefined
|
||||||
|
if (key.includes(' ')) return 'Spaces are not allowed'
|
||||||
|
if (!ENV_VAR_PATTERN.test(key)) return 'Only letters, numbers, and underscores allowed'
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
interface EnvironmentVariablesProps {
|
interface EnvironmentVariablesProps {
|
||||||
registerBeforeLeaveHandler?: (handler: (onProceed: () => void) => void) => void
|
registerBeforeLeaveHandler?: (handler: (onProceed: () => void) => void) => void
|
||||||
}
|
}
|
||||||
@@ -222,6 +233,10 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
|||||||
return envVars.some((envVar) => !!envVar.key && Object.hasOwn(workspaceVars, envVar.key))
|
return envVars.some((envVar) => !!envVar.key && Object.hasOwn(workspaceVars, envVar.key))
|
||||||
}, [envVars, workspaceVars])
|
}, [envVars, workspaceVars])
|
||||||
|
|
||||||
|
const hasInvalidKeys = useMemo(() => {
|
||||||
|
return envVars.some((envVar) => !!envVar.key && validateEnvVarKey(envVar.key))
|
||||||
|
}, [envVars])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hasChangesRef.current = hasChanges
|
hasChangesRef.current = hasChanges
|
||||||
}, [hasChanges])
|
}, [hasChanges])
|
||||||
@@ -551,6 +566,7 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
|||||||
const renderEnvVarRow = useCallback(
|
const renderEnvVarRow = useCallback(
|
||||||
(envVar: UIEnvironmentVariable, originalIndex: number) => {
|
(envVar: UIEnvironmentVariable, originalIndex: number) => {
|
||||||
const isConflict = !!envVar.key && Object.hasOwn(workspaceVars, envVar.key)
|
const isConflict = !!envVar.key && Object.hasOwn(workspaceVars, envVar.key)
|
||||||
|
const keyError = validateEnvVarKey(envVar.key)
|
||||||
const maskedValueStyle =
|
const maskedValueStyle =
|
||||||
focusedValueIndex !== originalIndex && !isConflict
|
focusedValueIndex !== originalIndex && !isConflict
|
||||||
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
|
? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties)
|
||||||
@@ -571,7 +587,7 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
|||||||
spellCheck='false'
|
spellCheck='false'
|
||||||
readOnly
|
readOnly
|
||||||
onFocus={(e) => e.target.removeAttribute('readOnly')}
|
onFocus={(e) => e.target.removeAttribute('readOnly')}
|
||||||
className={`h-9 ${isConflict ? conflictClassName : ''}`}
|
className={`h-9 ${isConflict ? conflictClassName : ''} ${keyError ? 'border-[var(--text-error)]' : ''}`}
|
||||||
/>
|
/>
|
||||||
<div />
|
<div />
|
||||||
<EmcnInput
|
<EmcnInput
|
||||||
@@ -627,7 +643,12 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
|||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isConflict && (
|
{keyError && (
|
||||||
|
<div className='col-span-3 mt-[4px] text-[12px] text-[var(--text-error)] leading-tight'>
|
||||||
|
{keyError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isConflict && !keyError && (
|
||||||
<div className='col-span-3 mt-[4px] text-[12px] text-[var(--text-error)] leading-tight'>
|
<div className='col-span-3 mt-[4px] text-[12px] text-[var(--text-error)] leading-tight'>
|
||||||
Workspace variable with the same name overrides this. Rename your personal key to use
|
Workspace variable with the same name overrides this. Rename your personal key to use
|
||||||
it.
|
it.
|
||||||
@@ -707,14 +728,17 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
|||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isLoading || !hasChanges || hasConflicts}
|
disabled={isLoading || !hasChanges || hasConflicts || hasInvalidKeys}
|
||||||
variant='primary'
|
variant='primary'
|
||||||
className={`${PRIMARY_BUTTON_STYLES} ${hasConflicts ? 'cursor-not-allowed opacity-50' : ''}`}
|
className={`${PRIMARY_BUTTON_STYLES} ${hasConflicts || hasInvalidKeys ? 'cursor-not-allowed opacity-50' : ''}`}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
{hasConflicts && <Tooltip.Content>Resolve all conflicts before saving</Tooltip.Content>}
|
{hasConflicts && <Tooltip.Content>Resolve all conflicts before saving</Tooltip.Content>}
|
||||||
|
{hasInvalidKeys && !hasConflicts && (
|
||||||
|
<Tooltip.Content>Fix invalid variable names before saving</Tooltip.Content>
|
||||||
|
)}
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -808,8 +832,8 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
|||||||
<ModalHeader>Unsaved Changes</ModalHeader>
|
<ModalHeader>Unsaved Changes</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||||
{hasConflicts
|
{hasConflicts || hasInvalidKeys
|
||||||
? 'You have unsaved changes, but conflicts must be resolved before saving. You can discard your changes to close the modal.'
|
? `You have unsaved changes, but ${hasConflicts ? 'conflicts must be resolved' : 'invalid variable names must be fixed'} before saving. You can discard your changes to close the modal.`
|
||||||
: 'You have unsaved changes. Do you want to save them before closing?'}
|
: 'You have unsaved changes. Do you want to save them before closing?'}
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@@ -817,18 +841,22 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
|||||||
<Button variant='default' onClick={handleCancel}>
|
<Button variant='default' onClick={handleCancel}>
|
||||||
Discard Changes
|
Discard Changes
|
||||||
</Button>
|
</Button>
|
||||||
{hasConflicts ? (
|
{hasConflicts || hasInvalidKeys ? (
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<Button
|
<Button
|
||||||
disabled={true}
|
disabled={true}
|
||||||
variant='primary'
|
variant='primary'
|
||||||
className='cursor-not-allowed opacity-50'
|
className={`${PRIMARY_BUTTON_STYLES} cursor-not-allowed opacity-50`}
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content>Resolve all conflicts before saving</Tooltip.Content>
|
<Tooltip.Content>
|
||||||
|
{hasConflicts
|
||||||
|
? 'Resolve all conflicts before saving'
|
||||||
|
: 'Fix invalid variable names before saving'}
|
||||||
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={handleSave} variant='primary' className={PRIMARY_BUTTON_STYLES}>
|
<Button onClick={handleSave} variant='primary' className={PRIMARY_BUTTON_STYLES}>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
|||||||
title: 'Notion Account',
|
title: 'Notion Account',
|
||||||
type: 'oauth-input',
|
type: 'oauth-input',
|
||||||
serviceId: 'notion',
|
serviceId: 'notion',
|
||||||
requiredScopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
|
|
||||||
placeholder: 'Select Notion account',
|
placeholder: 'Select Notion account',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import type { BlockHandler, ExecutionContext, PauseMetadata } from '@/executor/types'
|
import type { BlockHandler, ExecutionContext, PauseMetadata } from '@/executor/types'
|
||||||
import { collectBlockData } from '@/executor/utils/block-data'
|
import { collectBlockData } from '@/executor/utils/block-data'
|
||||||
import type { SerializedBlock } from '@/serializer/types'
|
import type { SerializedBlock } from '@/serializer/types'
|
||||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
import { executeTool } from '@/tools'
|
import { executeTool } from '@/tools'
|
||||||
|
|
||||||
const logger = createLogger('HumanInTheLoopBlockHandler')
|
const logger = createLogger('HumanInTheLoopBlockHandler')
|
||||||
@@ -591,7 +591,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
if (pauseBlockName) {
|
if (pauseBlockName) {
|
||||||
blockNameMappingWithPause[pauseBlockName] = pauseBlockId
|
blockNameMappingWithPause[pauseBlockName] = pauseBlockId
|
||||||
blockNameMappingWithPause[normalizeBlockName(pauseBlockName)] = pauseBlockId
|
blockNameMappingWithPause[normalizeName(pauseBlockName)] = pauseBlockId
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationPromises = tools.map<Promise<NotificationToolResult>>(async (toolConfig) => {
|
const notificationPromises = tools.map<Promise<NotificationToolResult>>(async (toolConfig) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type Resolver,
|
type Resolver,
|
||||||
} from '@/executor/variables/resolvers/reference'
|
} from '@/executor/variables/resolvers/reference'
|
||||||
import type { SerializedWorkflow } from '@/serializer/types'
|
import type { SerializedWorkflow } from '@/serializer/types'
|
||||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
|
|
||||||
export class BlockResolver implements Resolver {
|
export class BlockResolver implements Resolver {
|
||||||
private blockByNormalizedName: Map<string, string>
|
private blockByNormalizedName: Map<string, string>
|
||||||
@@ -15,7 +15,7 @@ export class BlockResolver implements Resolver {
|
|||||||
for (const block of workflow.blocks) {
|
for (const block of workflow.blocks) {
|
||||||
this.blockByNormalizedName.set(block.id, block.id)
|
this.blockByNormalizedName.set(block.id, block.id)
|
||||||
if (block.metadata?.name) {
|
if (block.metadata?.name) {
|
||||||
const normalized = normalizeBlockName(block.metadata.name)
|
const normalized = normalizeName(block.metadata.name)
|
||||||
this.blockByNormalizedName.set(normalized, block.id)
|
this.blockByNormalizedName.set(normalized, block.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export class BlockResolver implements Resolver {
|
|||||||
if (this.blockByNormalizedName.has(name)) {
|
if (this.blockByNormalizedName.has(name)) {
|
||||||
return this.blockByNormalizedName.get(name)
|
return this.blockByNormalizedName.get(name)
|
||||||
}
|
}
|
||||||
const normalized = normalizeBlockName(name)
|
const normalized = normalizeName(name)
|
||||||
return this.blockByNormalizedName.get(normalized)
|
return this.blockByNormalizedName.get(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
219
apps/sim/executor/variables/resolvers/workflow.test.ts
Normal file
219
apps/sim/executor/variables/resolvers/workflow.test.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import type { ResolutionContext } from './reference'
|
||||||
|
import { WorkflowResolver } from './workflow'
|
||||||
|
|
||||||
|
vi.mock('@/lib/logs/console/logger', () => ({
|
||||||
|
createLogger: vi.fn().mockReturnValue({
|
||||||
|
debug: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/lib/workflows/variables/variable-manager', () => ({
|
||||||
|
VariableManager: {
|
||||||
|
resolveForExecution: vi.fn((value) => value),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a minimal ResolutionContext for testing.
|
||||||
|
* The WorkflowResolver only uses context.executionContext.workflowVariables,
|
||||||
|
* so we only need to provide that field.
|
||||||
|
*/
|
||||||
|
function createTestContext(workflowVariables: Record<string, any>): ResolutionContext {
|
||||||
|
return {
|
||||||
|
executionContext: { workflowVariables },
|
||||||
|
executionState: {},
|
||||||
|
currentNodeId: 'test-node',
|
||||||
|
} as ResolutionContext
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WorkflowResolver', () => {
|
||||||
|
describe('canResolve', () => {
|
||||||
|
it.concurrent('should return true for variable references', () => {
|
||||||
|
const resolver = new WorkflowResolver({})
|
||||||
|
expect(resolver.canResolve('<variable.myvar>')).toBe(true)
|
||||||
|
expect(resolver.canResolve('<variable.test>')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return false for non-variable references', () => {
|
||||||
|
const resolver = new WorkflowResolver({})
|
||||||
|
expect(resolver.canResolve('<block.output>')).toBe(false)
|
||||||
|
expect(resolver.canResolve('<loop.index>')).toBe(false)
|
||||||
|
expect(resolver.canResolve('plain text')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resolve with normalized matching', () => {
|
||||||
|
it.concurrent('should resolve variable with exact name match', () => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: 'myvar', type: 'plain', value: 'test-value' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.myvar>', createTestContext(variables))
|
||||||
|
expect(result).toBe('test-value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should resolve variable with normalized name (lowercase)', () => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: 'MyVar', type: 'plain', value: 'test-value' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.myvar>', createTestContext(variables))
|
||||||
|
expect(result).toBe('test-value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should resolve variable with normalized name (spaces removed)', () => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: 'My Variable', type: 'plain', value: 'test-value' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.myvariable>', createTestContext(variables))
|
||||||
|
expect(result).toBe('test-value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent(
|
||||||
|
'should resolve variable with fully normalized name (JIRA TEAM UUID case)',
|
||||||
|
() => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: 'JIRA TEAM UUID', type: 'plain', value: 'uuid-123' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.jirateamuuid>', createTestContext(variables))
|
||||||
|
expect(result).toBe('uuid-123')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.concurrent('should resolve variable regardless of reference case', () => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: 'jirateamuuid', type: 'plain', value: 'uuid-123' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.JIRATEAMUUID>', createTestContext(variables))
|
||||||
|
expect(result).toBe('uuid-123')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should resolve by variable ID (exact match)', () => {
|
||||||
|
const variables = {
|
||||||
|
'my-uuid-id': { id: 'my-uuid-id', name: 'Some Name', type: 'plain', value: 'id-value' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.my-uuid-id>', createTestContext(variables))
|
||||||
|
expect(result).toBe('id-value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return undefined for non-existent variable', () => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: 'existing', type: 'plain', value: 'test' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.nonexistent>', createTestContext(variables))
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle nested path access', () => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': {
|
||||||
|
id: 'var-1',
|
||||||
|
name: 'config',
|
||||||
|
type: 'object',
|
||||||
|
value: { nested: { value: 'deep' } },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve(
|
||||||
|
'<variable.config.nested.value>',
|
||||||
|
createTestContext(variables)
|
||||||
|
)
|
||||||
|
expect(result).toBe('deep')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should resolve with mixed case and spaces in reference', () => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: 'api key', type: 'plain', value: 'secret-key' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.APIKEY>', createTestContext(variables))
|
||||||
|
expect(result).toBe('secret-key')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle real-world variable naming patterns', () => {
|
||||||
|
const testCases = [
|
||||||
|
{ varName: 'User ID', refName: 'userid', value: 'user-123' },
|
||||||
|
{ varName: 'API Key', refName: 'apikey', value: 'key-456' },
|
||||||
|
{ varName: 'STRIPE SECRET KEY', refName: 'stripesecretkey', value: 'sk_test' },
|
||||||
|
{ varName: 'Database URL', refName: 'databaseurl', value: 'postgres://...' },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const { varName, refName, value } of testCases) {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: varName, type: 'plain', value },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve(`<variable.${refName}>`, createTestContext(variables))
|
||||||
|
expect(result).toBe(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it.concurrent('should handle empty workflow variables', () => {
|
||||||
|
const resolver = new WorkflowResolver({})
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.anyvar>', createTestContext({}))
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle invalid reference format', () => {
|
||||||
|
const resolver = new WorkflowResolver({})
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable>', createTestContext({}))
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle null variable values in the map', () => {
|
||||||
|
const variables: Record<string, any> = {
|
||||||
|
'var-1': null,
|
||||||
|
'var-2': { id: 'var-2', name: 'valid', type: 'plain', value: 'exists' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.valid>', createTestContext(variables))
|
||||||
|
expect(result).toBe('exists')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle variable with empty name', () => {
|
||||||
|
const variables = {
|
||||||
|
'var-1': { id: 'var-1', name: '', type: 'plain', value: 'empty-name' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
// Empty name normalizes to empty string, which matches "<variable.>" reference
|
||||||
|
const result = resolver.resolve('<variable.>', createTestContext(variables))
|
||||||
|
expect(result).toBe('empty-name')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should prefer name match over ID match when both could apply', () => {
|
||||||
|
const variables = {
|
||||||
|
apikey: { id: 'apikey', name: 'different', type: 'plain', value: 'by-id' },
|
||||||
|
'var-2': { id: 'var-2', name: 'apikey', type: 'plain', value: 'by-name' },
|
||||||
|
}
|
||||||
|
const resolver = new WorkflowResolver(variables)
|
||||||
|
|
||||||
|
const result = resolver.resolve('<variable.apikey>', createTestContext(variables))
|
||||||
|
expect(['by-id', 'by-name']).toContain(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type ResolutionContext,
|
type ResolutionContext,
|
||||||
type Resolver,
|
type Resolver,
|
||||||
} from '@/executor/variables/resolvers/reference'
|
} from '@/executor/variables/resolvers/reference'
|
||||||
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
|
|
||||||
const logger = createLogger('WorkflowResolver')
|
const logger = createLogger('WorkflowResolver')
|
||||||
|
|
||||||
@@ -32,12 +33,17 @@ export class WorkflowResolver implements Resolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [_, variableName, ...pathParts] = parts
|
const [_, variableName, ...pathParts] = parts
|
||||||
|
const normalizedRefName = normalizeName(variableName)
|
||||||
|
|
||||||
const workflowVars = context.executionContext.workflowVariables || this.workflowVariables
|
const workflowVars = context.executionContext.workflowVariables || this.workflowVariables
|
||||||
|
|
||||||
for (const varObj of Object.values(workflowVars)) {
|
for (const varObj of Object.values(workflowVars)) {
|
||||||
const v = varObj as any
|
const v = varObj as any
|
||||||
if (v && (v.name === variableName || v.id === variableName)) {
|
if (!v) continue
|
||||||
|
|
||||||
|
// Match by normalized name or exact ID
|
||||||
|
const normalizedVarName = v.name ? normalizeName(v.name) : ''
|
||||||
|
if (normalizedVarName === normalizedRefName || v.id === variableName) {
|
||||||
const normalizedType = (v.type === 'string' ? 'plain' : v.type) || 'plain'
|
const normalizedType = (v.type === 'string' ? 'plain' : v.type) || 'plain'
|
||||||
let value: any
|
let value: any
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1288,7 +1288,6 @@ export function useCollaborativeWorkflow() {
|
|||||||
},
|
},
|
||||||
workflowId: activeWorkflowId || '',
|
workflowId: activeWorkflowId || '',
|
||||||
userId: session?.user?.id || 'unknown',
|
userId: session?.user?.id || 'unknown',
|
||||||
immediate: true,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ export const auth = betterAuth({
|
|||||||
'github',
|
'github',
|
||||||
'email-password',
|
'email-password',
|
||||||
'confluence',
|
'confluence',
|
||||||
// 'supabase',
|
|
||||||
'x',
|
'x',
|
||||||
'notion',
|
'notion',
|
||||||
'microsoft',
|
'microsoft',
|
||||||
@@ -950,56 +949,6 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Supabase provider (unused)
|
|
||||||
// {
|
|
||||||
// providerId: 'supabase',
|
|
||||||
// clientId: env.SUPABASE_CLIENT_ID as string,
|
|
||||||
// clientSecret: env.SUPABASE_CLIENT_SECRET as string,
|
|
||||||
// authorizationUrl: 'https://api.supabase.com/v1/oauth/authorize',
|
|
||||||
// tokenUrl: 'https://api.supabase.com/v1/oauth/token',
|
|
||||||
// userInfoUrl: 'https://dummy-not-used.supabase.co',
|
|
||||||
// scopes: ['database.read', 'database.write', 'projects.read'],
|
|
||||||
// responseType: 'code',
|
|
||||||
// pkce: true,
|
|
||||||
// redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/supabase`,
|
|
||||||
// getUserInfo: async (tokens) => {
|
|
||||||
// try {
|
|
||||||
// logger.info('Creating Supabase user profile from token data')
|
|
||||||
|
|
||||||
// let userId = 'supabase-user'
|
|
||||||
// if (tokens.idToken) {
|
|
||||||
// try {
|
|
||||||
// const decodedToken = JSON.parse(
|
|
||||||
// Buffer.from(tokens.idToken.split('.')[1], 'base64').toString()
|
|
||||||
// )
|
|
||||||
// if (decodedToken.sub) {
|
|
||||||
// userId = decodedToken.sub
|
|
||||||
// }
|
|
||||||
// } catch (e) {
|
|
||||||
// logger.warn('Failed to decode Supabase ID token', {
|
|
||||||
// error: e,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const uniqueId = `${userId}-${Date.now()}`
|
|
||||||
// const now = new Date()
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// id: uniqueId,
|
|
||||||
// name: 'Supabase User',
|
|
||||||
// email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@supabase.user`,
|
|
||||||
// emailVerified: false,
|
|
||||||
// createdAt: now,
|
|
||||||
// updatedAt: now,
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.error('Error creating Supabase user profile:', { error })
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
|
|
||||||
// X provider
|
// X provider
|
||||||
{
|
{
|
||||||
providerId: 'x',
|
providerId: 'x',
|
||||||
@@ -1133,57 +1082,6 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Discord provider (unused)
|
|
||||||
// {
|
|
||||||
// providerId: 'discord',
|
|
||||||
// clientId: env.DISCORD_CLIENT_ID as string,
|
|
||||||
// clientSecret: env.DISCORD_CLIENT_SECRET as string,
|
|
||||||
// authorizationUrl: 'https://discord.com/api/oauth2/authorize',
|
|
||||||
// tokenUrl: 'https://discord.com/api/oauth2/token',
|
|
||||||
// userInfoUrl: 'https://discord.com/api/users/@me',
|
|
||||||
// scopes: ['identify', 'bot', 'messages.read', 'guilds', 'guilds.members.read'],
|
|
||||||
// responseType: 'code',
|
|
||||||
// accessType: 'offline',
|
|
||||||
// authentication: 'basic',
|
|
||||||
// prompt: 'consent',
|
|
||||||
// redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/discord`,
|
|
||||||
// getUserInfo: async (tokens) => {
|
|
||||||
// try {
|
|
||||||
// const response = await fetch('https://discord.com/api/users/@me', {
|
|
||||||
// headers: {
|
|
||||||
// Authorization: `Bearer ${tokens.accessToken}`,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
// if (!response.ok) {
|
|
||||||
// logger.error('Error fetching Discord user info:', {
|
|
||||||
// status: response.status,
|
|
||||||
// statusText: response.statusText,
|
|
||||||
// })
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const profile = await response.json()
|
|
||||||
// const now = new Date()
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// id: profile.id,
|
|
||||||
// name: profile.username || 'Discord User',
|
|
||||||
// email: profile.email || `${profile.id}@discord.user`,
|
|
||||||
// image: profile.avatar
|
|
||||||
// ? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`
|
|
||||||
// : undefined,
|
|
||||||
// emailVerified: profile.verified || false,
|
|
||||||
// createdAt: now,
|
|
||||||
// updatedAt: now,
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.error('Error in Discord getUserInfo:', { error })
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
|
|
||||||
// Jira provider
|
// Jira provider
|
||||||
{
|
{
|
||||||
providerId: 'jira',
|
providerId: 'jira',
|
||||||
@@ -1323,7 +1221,6 @@ export const auth = betterAuth({
|
|||||||
authorizationUrl: 'https://api.notion.com/v1/oauth/authorize',
|
authorizationUrl: 'https://api.notion.com/v1/oauth/authorize',
|
||||||
tokenUrl: 'https://api.notion.com/v1/oauth/token',
|
tokenUrl: 'https://api.notion.com/v1/oauth/token',
|
||||||
userInfoUrl: 'https://api.notion.com/v1/users/me',
|
userInfoUrl: 'https://api.notion.com/v1/users/me',
|
||||||
scopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
|
|
||||||
responseType: 'code',
|
responseType: 'code',
|
||||||
pkce: false,
|
pkce: false,
|
||||||
accessType: 'offline',
|
accessType: 'offline',
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
AirtableIcon,
|
AirtableIcon,
|
||||||
AsanaIcon,
|
AsanaIcon,
|
||||||
ConfluenceIcon,
|
ConfluenceIcon,
|
||||||
// DiscordIcon,
|
|
||||||
DropboxIcon,
|
DropboxIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
GmailIcon,
|
GmailIcon,
|
||||||
@@ -32,7 +31,6 @@ import {
|
|||||||
ShopifyIcon,
|
ShopifyIcon,
|
||||||
SlackIcon,
|
SlackIcon,
|
||||||
SpotifyIcon,
|
SpotifyIcon,
|
||||||
// SupabaseIcon,
|
|
||||||
TrelloIcon,
|
TrelloIcon,
|
||||||
WealthboxIcon,
|
WealthboxIcon,
|
||||||
WebflowIcon,
|
WebflowIcon,
|
||||||
@@ -49,12 +47,10 @@ export type OAuthProvider =
|
|||||||
| 'google'
|
| 'google'
|
||||||
| 'github'
|
| 'github'
|
||||||
| 'x'
|
| 'x'
|
||||||
// | 'supabase'
|
|
||||||
| 'confluence'
|
| 'confluence'
|
||||||
| 'airtable'
|
| 'airtable'
|
||||||
| 'notion'
|
| 'notion'
|
||||||
| 'jira'
|
| 'jira'
|
||||||
// | 'discord'
|
|
||||||
| 'dropbox'
|
| 'dropbox'
|
||||||
| 'microsoft'
|
| 'microsoft'
|
||||||
| 'linear'
|
| 'linear'
|
||||||
@@ -86,12 +82,10 @@ export type OAuthService =
|
|||||||
| 'google-groups'
|
| 'google-groups'
|
||||||
| 'github'
|
| 'github'
|
||||||
| 'x'
|
| 'x'
|
||||||
// | 'supabase'
|
|
||||||
| 'confluence'
|
| 'confluence'
|
||||||
| 'airtable'
|
| 'airtable'
|
||||||
| 'notion'
|
| 'notion'
|
||||||
| 'jira'
|
| 'jira'
|
||||||
// | 'discord'
|
|
||||||
| 'dropbox'
|
| 'dropbox'
|
||||||
| 'microsoft-excel'
|
| 'microsoft-excel'
|
||||||
| 'microsoft-teams'
|
| 'microsoft-teams'
|
||||||
@@ -388,23 +382,6 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
|||||||
},
|
},
|
||||||
defaultService: 'x',
|
defaultService: 'x',
|
||||||
},
|
},
|
||||||
// supabase: {
|
|
||||||
// id: 'supabase',
|
|
||||||
// name: 'Supabase',
|
|
||||||
// icon: (props) => SupabaseIcon(props),
|
|
||||||
// services: {
|
|
||||||
// supabase: {
|
|
||||||
// id: 'supabase',
|
|
||||||
// name: 'Supabase',
|
|
||||||
// description: 'Connect to your Supabase projects and manage data.',
|
|
||||||
// providerId: 'supabase',
|
|
||||||
// icon: (props) => SupabaseIcon(props),
|
|
||||||
// baseProviderIcon: (props) => SupabaseIcon(props),
|
|
||||||
// scopes: ['database.read', 'database.write', 'projects.read'],
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// defaultService: 'supabase',
|
|
||||||
// },
|
|
||||||
confluence: {
|
confluence: {
|
||||||
id: 'confluence',
|
id: 'confluence',
|
||||||
name: 'Confluence',
|
name: 'Confluence',
|
||||||
@@ -518,23 +495,6 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
|||||||
},
|
},
|
||||||
defaultService: 'airtable',
|
defaultService: 'airtable',
|
||||||
},
|
},
|
||||||
// discord: {
|
|
||||||
// id: 'discord',
|
|
||||||
// name: 'Discord',
|
|
||||||
// icon: (props) => DiscordIcon(props),
|
|
||||||
// services: {
|
|
||||||
// discord: {
|
|
||||||
// id: 'discord',
|
|
||||||
// name: 'Discord',
|
|
||||||
// description: 'Read and send messages to Discord channels and interact with servers.',
|
|
||||||
// providerId: 'discord',
|
|
||||||
// icon: (props) => DiscordIcon(props),
|
|
||||||
// baseProviderIcon: (props) => DiscordIcon(props),
|
|
||||||
// scopes: ['identify', 'bot', 'messages.read', 'guilds', 'guilds.members.read'],
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// defaultService: 'discord',
|
|
||||||
// },
|
|
||||||
notion: {
|
notion: {
|
||||||
id: 'notion',
|
id: 'notion',
|
||||||
name: 'Notion',
|
name: 'Notion',
|
||||||
@@ -547,7 +507,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
|||||||
providerId: 'notion',
|
providerId: 'notion',
|
||||||
icon: (props) => NotionIcon(props),
|
icon: (props) => NotionIcon(props),
|
||||||
baseProviderIcon: (props) => NotionIcon(props),
|
baseProviderIcon: (props) => NotionIcon(props),
|
||||||
scopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
|
scopes: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultService: 'notion',
|
defaultService: 'notion',
|
||||||
@@ -1272,18 +1232,6 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
|
|||||||
useBasicAuth: false,
|
useBasicAuth: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// case 'discord': {
|
|
||||||
// const { clientId, clientSecret } = getCredentials(
|
|
||||||
// env.DISCORD_CLIENT_ID,
|
|
||||||
// env.DISCORD_CLIENT_SECRET
|
|
||||||
// )
|
|
||||||
// return {
|
|
||||||
// tokenEndpoint: 'https://discord.com/api/v10/oauth2/token',
|
|
||||||
// clientId,
|
|
||||||
// clientSecret,
|
|
||||||
// useBasicAuth: true,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
case 'microsoft': {
|
case 'microsoft': {
|
||||||
const { clientId, clientSecret } = getCredentials(
|
const { clientId, clientSecret } = getCredentials(
|
||||||
env.MICROSOFT_CLIENT_ID,
|
env.MICROSOFT_CLIENT_ID,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ interface EnqueueWorkflowOperationArgs {
|
|||||||
target: string
|
target: string
|
||||||
payload: any
|
payload: any
|
||||||
workflowId: string
|
workflowId: string
|
||||||
immediate?: boolean
|
|
||||||
operationId?: string
|
operationId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +36,6 @@ export async function enqueueWorkflowOperation({
|
|||||||
target,
|
target,
|
||||||
payload,
|
payload,
|
||||||
workflowId,
|
workflowId,
|
||||||
immediate = false,
|
|
||||||
operationId,
|
operationId,
|
||||||
}: EnqueueWorkflowOperationArgs): Promise<string> {
|
}: EnqueueWorkflowOperationArgs): Promise<string> {
|
||||||
const userId = await resolveUserId()
|
const userId = await resolveUserId()
|
||||||
@@ -52,7 +50,6 @@ export async function enqueueWorkflowOperation({
|
|||||||
},
|
},
|
||||||
workflowId,
|
workflowId,
|
||||||
userId,
|
userId,
|
||||||
immediate,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.debug('Queued workflow operation', {
|
logger.debug('Queued workflow operation', {
|
||||||
@@ -60,7 +57,6 @@ export async function enqueueWorkflowOperation({
|
|||||||
operation,
|
operation,
|
||||||
target,
|
target,
|
||||||
operationId: opId,
|
operationId: opId,
|
||||||
immediate,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return opId
|
return opId
|
||||||
@@ -69,7 +65,6 @@ export async function enqueueWorkflowOperation({
|
|||||||
interface EnqueueReplaceStateArgs {
|
interface EnqueueReplaceStateArgs {
|
||||||
workflowId: string
|
workflowId: string
|
||||||
state: WorkflowState
|
state: WorkflowState
|
||||||
immediate?: boolean
|
|
||||||
operationId?: string
|
operationId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +74,6 @@ interface EnqueueReplaceStateArgs {
|
|||||||
export async function enqueueReplaceWorkflowState({
|
export async function enqueueReplaceWorkflowState({
|
||||||
workflowId,
|
workflowId,
|
||||||
state,
|
state,
|
||||||
immediate,
|
|
||||||
operationId,
|
operationId,
|
||||||
}: EnqueueReplaceStateArgs): Promise<string> {
|
}: EnqueueReplaceStateArgs): Promise<string> {
|
||||||
return enqueueWorkflowOperation({
|
return enqueueWorkflowOperation({
|
||||||
@@ -87,7 +81,6 @@ export async function enqueueReplaceWorkflowState({
|
|||||||
operation: 'replace-state',
|
operation: 'replace-state',
|
||||||
target: 'workflow',
|
target: 'workflow',
|
||||||
payload: { state },
|
payload: { state },
|
||||||
immediate,
|
|
||||||
operationId,
|
operationId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { normalizeBlockName } from '@/stores/workflows/utils'
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
|
|
||||||
export const SYSTEM_REFERENCE_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable'])
|
export const SYSTEM_REFERENCE_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable'])
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export function extractReferencePrefixes(value: string): Array<{ raw: string; pr
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = normalizeBlockName(rawPrefix)
|
const normalized = normalizeName(rawPrefix)
|
||||||
references.push({ raw: referenceSegment, prefix: normalized })
|
references.push({ raw: referenceSegment, prefix: normalized })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface QueuedOperation {
|
|||||||
retryCount: number
|
retryCount: number
|
||||||
status: 'pending' | 'processing' | 'confirmed' | 'failed'
|
status: 'pending' | 'processing' | 'confirmed' | 'failed'
|
||||||
userId: string
|
userId: string
|
||||||
immediate?: boolean // Flag for immediate processing (skips debouncing)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OperationQueueState {
|
interface OperationQueueState {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { devtools } from 'zustand/middleware'
|
import { devtools } from 'zustand/middleware'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
|
import { useOperationQueueStore } from '@/stores/operation-queue/store'
|
||||||
import type { Variable, VariablesStore } from '@/stores/panel/variables/types'
|
import type { Variable, VariablesStore } from '@/stores/panel/variables/types'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
|
import { normalizeName } from '@/stores/workflows/utils'
|
||||||
|
|
||||||
const logger = createLogger('VariablesStore')
|
const logger = createLogger('VariablesStore')
|
||||||
|
|
||||||
@@ -177,49 +179,72 @@ export const useVariablesStore = create<VariablesStore>()(
|
|||||||
if (activeWorkflowId) {
|
if (activeWorkflowId) {
|
||||||
const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {}
|
const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {}
|
||||||
const updatedWorkflowValues = { ...workflowValues }
|
const updatedWorkflowValues = { ...workflowValues }
|
||||||
|
const changedSubBlocks: Array<{ blockId: string; subBlockId: string; value: any }> =
|
||||||
|
[]
|
||||||
|
|
||||||
|
const oldVarName = normalizeName(oldVariableName)
|
||||||
|
const newVarName = normalizeName(newName)
|
||||||
|
const regex = new RegExp(`<variable\\.${oldVarName}>`, 'gi')
|
||||||
|
|
||||||
|
const updateReferences = (value: any, pattern: RegExp, replacement: string): any => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return pattern.test(value) ? value.replace(pattern, replacement) : value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => updateReferences(item, pattern, replacement))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== null && typeof value === 'object') {
|
||||||
|
const result = { ...value }
|
||||||
|
for (const key in result) {
|
||||||
|
result[key] = updateReferences(result[key], pattern, replacement)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
|
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
|
||||||
Object.entries(blockValues as Record<string, any>).forEach(
|
Object.entries(blockValues as Record<string, any>).forEach(
|
||||||
([subBlockId, value]) => {
|
([subBlockId, value]) => {
|
||||||
const oldVarName = oldVariableName.replace(/\s+/g, '').toLowerCase()
|
const updatedValue = updateReferences(value, regex, `<variable.${newVarName}>`)
|
||||||
const newVarName = newName.replace(/\s+/g, '').toLowerCase()
|
|
||||||
const regex = new RegExp(`<variable\.${oldVarName}>`, 'gi')
|
|
||||||
|
|
||||||
updatedWorkflowValues[blockId][subBlockId] = updateReferences(
|
if (JSON.stringify(updatedValue) !== JSON.stringify(value)) {
|
||||||
value,
|
if (!updatedWorkflowValues[blockId]) {
|
||||||
regex,
|
updatedWorkflowValues[blockId] = { ...workflowValues[blockId] }
|
||||||
`<variable.${newVarName}>`
|
|
||||||
)
|
|
||||||
|
|
||||||
function updateReferences(value: any, regex: RegExp, replacement: string): any {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return regex.test(value) ? value.replace(regex, replacement) : value
|
|
||||||
}
|
}
|
||||||
|
updatedWorkflowValues[blockId][subBlockId] = updatedValue
|
||||||
if (Array.isArray(value)) {
|
changedSubBlocks.push({ blockId, subBlockId, value: updatedValue })
|
||||||
return value.map((item) => updateReferences(item, regex, replacement))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== null && typeof value === 'object') {
|
|
||||||
const result = { ...value }
|
|
||||||
for (const key in result) {
|
|
||||||
result[key] = updateReferences(result[key], regex, replacement)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update local state
|
||||||
useSubBlockStore.setState({
|
useSubBlockStore.setState({
|
||||||
workflowValues: {
|
workflowValues: {
|
||||||
...subBlockStore.workflowValues,
|
...subBlockStore.workflowValues,
|
||||||
[activeWorkflowId]: updatedWorkflowValues,
|
[activeWorkflowId]: updatedWorkflowValues,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Queue operations for persistence via socket
|
||||||
|
const operationQueue = useOperationQueueStore.getState()
|
||||||
|
|
||||||
|
for (const { blockId, subBlockId, value } of changedSubBlocks) {
|
||||||
|
operationQueue.addToQueue({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
operation: {
|
||||||
|
operation: 'subblock-update',
|
||||||
|
target: 'subblock',
|
||||||
|
payload: { blockId, subblockId: subBlockId, value },
|
||||||
|
},
|
||||||
|
workflowId: activeWorkflowId,
|
||||||
|
userId: 'system',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -499,7 +499,6 @@ export const useWorkflowDiffStore = create<WorkflowDiffState & WorkflowDiffActio
|
|||||||
await enqueueReplaceWorkflowState({
|
await enqueueReplaceWorkflowState({
|
||||||
workflowId: activeWorkflowId,
|
workflowId: activeWorkflowId,
|
||||||
state: baselineWorkflow,
|
state: baselineWorkflow,
|
||||||
immediate: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Persist to database
|
// Persist to database
|
||||||
|
|||||||
93
apps/sim/stores/workflows/utils.test.ts
Normal file
93
apps/sim/stores/workflows/utils.test.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { normalizeName } from './utils'
|
||||||
|
|
||||||
|
describe('normalizeName', () => {
|
||||||
|
it.concurrent('should convert to lowercase', () => {
|
||||||
|
expect(normalizeName('MyVariable')).toBe('myvariable')
|
||||||
|
expect(normalizeName('UPPERCASE')).toBe('uppercase')
|
||||||
|
expect(normalizeName('MixedCase')).toBe('mixedcase')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should remove spaces', () => {
|
||||||
|
expect(normalizeName('my variable')).toBe('myvariable')
|
||||||
|
expect(normalizeName('my variable')).toBe('myvariable')
|
||||||
|
expect(normalizeName(' spaced ')).toBe('spaced')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle both lowercase and space removal', () => {
|
||||||
|
expect(normalizeName('JIRA TEAM UUID')).toBe('jirateamuuid')
|
||||||
|
expect(normalizeName('My Block Name')).toBe('myblockname')
|
||||||
|
expect(normalizeName('API 1')).toBe('api1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle edge cases', () => {
|
||||||
|
expect(normalizeName('')).toBe('')
|
||||||
|
expect(normalizeName(' ')).toBe('')
|
||||||
|
expect(normalizeName('a')).toBe('a')
|
||||||
|
expect(normalizeName('already_normalized')).toBe('already_normalized')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should preserve non-space special characters', () => {
|
||||||
|
expect(normalizeName('my-variable')).toBe('my-variable')
|
||||||
|
expect(normalizeName('my_variable')).toBe('my_variable')
|
||||||
|
expect(normalizeName('my.variable')).toBe('my.variable')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle tabs and newlines as whitespace', () => {
|
||||||
|
expect(normalizeName('my\tvariable')).toBe('myvariable')
|
||||||
|
expect(normalizeName('my\nvariable')).toBe('myvariable')
|
||||||
|
expect(normalizeName('my\r\nvariable')).toBe('myvariable')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle unicode characters', () => {
|
||||||
|
expect(normalizeName('Café')).toBe('café')
|
||||||
|
expect(normalizeName('日本語')).toBe('日本語')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should normalize block names correctly', () => {
|
||||||
|
expect(normalizeName('Agent 1')).toBe('agent1')
|
||||||
|
expect(normalizeName('API Block')).toBe('apiblock')
|
||||||
|
expect(normalizeName('My Custom Block')).toBe('mycustomblock')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should normalize variable names correctly', () => {
|
||||||
|
expect(normalizeName('jira1')).toBe('jira1')
|
||||||
|
expect(normalizeName('JIRA TEAM UUID')).toBe('jirateamuuid')
|
||||||
|
expect(normalizeName('My Variable')).toBe('myvariable')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should produce consistent results for references', () => {
|
||||||
|
const originalName = 'JIRA TEAM UUID'
|
||||||
|
const normalized1 = normalizeName(originalName)
|
||||||
|
const normalized2 = normalizeName(originalName)
|
||||||
|
|
||||||
|
expect(normalized1).toBe(normalized2)
|
||||||
|
expect(normalized1).toBe('jirateamuuid')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should allow matching block references to variable references', () => {
|
||||||
|
const name = 'API Block'
|
||||||
|
const blockRef = `<${normalizeName(name)}.output>`
|
||||||
|
const varRef = `<variable.${normalizeName(name)}>`
|
||||||
|
|
||||||
|
expect(blockRef).toBe('<apiblock.output>')
|
||||||
|
expect(varRef).toBe('<variable.apiblock>')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should handle real-world naming patterns consistently', () => {
|
||||||
|
const realWorldNames = [
|
||||||
|
{ input: 'User ID', expected: 'userid' },
|
||||||
|
{ input: 'API Key', expected: 'apikey' },
|
||||||
|
{ input: 'OAuth Token', expected: 'oauthtoken' },
|
||||||
|
{ input: 'Database URL', expected: 'databaseurl' },
|
||||||
|
{ input: 'STRIPE SECRET KEY', expected: 'stripesecretkey' },
|
||||||
|
{ input: 'openai api key', expected: 'openaiapikey' },
|
||||||
|
{ input: 'Customer Name', expected: 'customername' },
|
||||||
|
{ input: 'Order Total', expected: 'ordertotal' },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const { input, expected } of realWorldNames) {
|
||||||
|
expect(normalizeName(input)).toBe(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,11 +2,12 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
|||||||
import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types'
|
import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes a block name for comparison by converting to lowercase and removing spaces
|
* Normalizes a name for comparison by converting to lowercase and removing spaces.
|
||||||
* @param name - The block name to normalize
|
* Used for both block names and variable names to ensure consistent matching.
|
||||||
|
* @param name - The name to normalize
|
||||||
* @returns The normalized name
|
* @returns The normalized name
|
||||||
*/
|
*/
|
||||||
export function normalizeBlockName(name: string): string {
|
export function normalizeName(name: string): string {
|
||||||
return name.toLowerCase().replace(/\s+/g, '')
|
return name.toLowerCase().replace(/\s+/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ export function normalizeBlockName(name: string): string {
|
|||||||
export function getUniqueBlockName(baseName: string, existingBlocks: Record<string, any>): string {
|
export function getUniqueBlockName(baseName: string, existingBlocks: Record<string, any>): string {
|
||||||
// Special case: Start blocks should always be named "Start" without numbers
|
// Special case: Start blocks should always be named "Start" without numbers
|
||||||
// This applies to both "Start" and "Starter" base names
|
// This applies to both "Start" and "Starter" base names
|
||||||
const normalizedBaseName = normalizeBlockName(baseName)
|
const normalizedBaseName = normalizeName(baseName)
|
||||||
if (normalizedBaseName === 'start' || normalizedBaseName === 'starter') {
|
if (normalizedBaseName === 'start' || normalizedBaseName === 'starter') {
|
||||||
return 'Start'
|
return 'Start'
|
||||||
}
|
}
|
||||||
@@ -28,13 +29,13 @@ export function getUniqueBlockName(baseName: string, existingBlocks: Record<stri
|
|||||||
const baseNameMatch = baseName.match(/^(.*?)(\s+\d+)?$/)
|
const baseNameMatch = baseName.match(/^(.*?)(\s+\d+)?$/)
|
||||||
const namePrefix = baseNameMatch ? baseNameMatch[1].trim() : baseName
|
const namePrefix = baseNameMatch ? baseNameMatch[1].trim() : baseName
|
||||||
|
|
||||||
const normalizedBase = normalizeBlockName(namePrefix)
|
const normalizedBase = normalizeName(namePrefix)
|
||||||
|
|
||||||
const existingNumbers = Object.values(existingBlocks)
|
const existingNumbers = Object.values(existingBlocks)
|
||||||
.filter((block) => {
|
.filter((block) => {
|
||||||
const blockNameMatch = block.name?.match(/^(.*?)(\s+\d+)?$/)
|
const blockNameMatch = block.name?.match(/^(.*?)(\s+\d+)?$/)
|
||||||
const blockPrefix = blockNameMatch ? blockNameMatch[1].trim() : block.name
|
const blockPrefix = blockNameMatch ? blockNameMatch[1].trim() : block.name
|
||||||
return blockPrefix && normalizeBlockName(blockPrefix) === normalizedBase
|
return blockPrefix && normalizeName(blockPrefix) === normalizedBase
|
||||||
})
|
})
|
||||||
.map((block) => {
|
.map((block) => {
|
||||||
const match = block.name?.match(/(\d+)$/)
|
const match = block.name?.match(/(\d+)$/)
|
||||||
@@ -65,45 +66,34 @@ export function mergeSubblockState(
|
|||||||
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
||||||
const subBlockStore = useSubBlockStore.getState()
|
const subBlockStore = useSubBlockStore.getState()
|
||||||
|
|
||||||
// Get all the values stored in the subblock store for this workflow
|
|
||||||
const workflowSubblockValues = workflowId ? subBlockStore.workflowValues[workflowId] || {} : {}
|
const workflowSubblockValues = workflowId ? subBlockStore.workflowValues[workflowId] || {} : {}
|
||||||
|
|
||||||
return Object.entries(blocksToProcess).reduce(
|
return Object.entries(blocksToProcess).reduce(
|
||||||
(acc, [id, block]) => {
|
(acc, [id, block]) => {
|
||||||
// Skip if block is undefined
|
|
||||||
if (!block) {
|
if (!block) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize subBlocks if not present
|
|
||||||
const blockSubBlocks = block.subBlocks || {}
|
const blockSubBlocks = block.subBlocks || {}
|
||||||
|
|
||||||
// Get stored values for this block
|
|
||||||
const blockValues = workflowSubblockValues[id] || {}
|
const blockValues = workflowSubblockValues[id] || {}
|
||||||
|
|
||||||
// Create a deep copy of the block's subBlocks to maintain structure
|
|
||||||
const mergedSubBlocks = Object.entries(blockSubBlocks).reduce(
|
const mergedSubBlocks = Object.entries(blockSubBlocks).reduce(
|
||||||
(subAcc, [subBlockId, subBlock]) => {
|
(subAcc, [subBlockId, subBlock]) => {
|
||||||
// Skip if subBlock is undefined
|
|
||||||
if (!subBlock) {
|
if (!subBlock) {
|
||||||
return subAcc
|
return subAcc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the stored value for this subblock
|
|
||||||
let storedValue = null
|
let storedValue = null
|
||||||
|
|
||||||
// If workflowId is provided, use it to get the value
|
|
||||||
if (workflowId) {
|
if (workflowId) {
|
||||||
// Try to get the value from the subblock store for this specific workflow
|
|
||||||
if (blockValues[subBlockId] !== undefined) {
|
if (blockValues[subBlockId] !== undefined) {
|
||||||
storedValue = blockValues[subBlockId]
|
storedValue = blockValues[subBlockId]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fall back to the active workflow if no workflowId is provided
|
|
||||||
storedValue = subBlockStore.getValue(id, subBlockId)
|
storedValue = subBlockStore.getValue(id, subBlockId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new subblock object with the same structure but updated value
|
|
||||||
subAcc[subBlockId] = {
|
subAcc[subBlockId] = {
|
||||||
...subBlock,
|
...subBlock,
|
||||||
value: storedValue !== undefined && storedValue !== null ? storedValue : subBlock.value,
|
value: storedValue !== undefined && storedValue !== null ? storedValue : subBlock.value,
|
||||||
@@ -200,7 +190,24 @@ export async function mergeSubblockStateAsync(
|
|||||||
subBlockEntries.filter((entry): entry is readonly [string, SubBlockState] => entry !== null)
|
subBlockEntries.filter((entry): entry is readonly [string, SubBlockState] => entry !== null)
|
||||||
) as Record<string, SubBlockState>
|
) as Record<string, SubBlockState>
|
||||||
|
|
||||||
// Return the full block state with updated subBlocks
|
// Add any values that exist in the store but aren't in the block structure
|
||||||
|
// This handles cases where block config has been updated but values still exist
|
||||||
|
// IMPORTANT: This includes runtime subblock IDs like webhookId, triggerPath, etc.
|
||||||
|
if (workflowId) {
|
||||||
|
const workflowValues = subBlockStore.workflowValues[workflowId]
|
||||||
|
const blockValues = workflowValues?.[id] || {}
|
||||||
|
Object.entries(blockValues).forEach(([subBlockId, value]) => {
|
||||||
|
if (!mergedSubBlocks[subBlockId] && value !== null && value !== undefined) {
|
||||||
|
mergedSubBlocks[subBlockId] = {
|
||||||
|
id: subBlockId,
|
||||||
|
type: 'short-input',
|
||||||
|
value: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the full block state with updated subBlocks (including orphaned values)
|
||||||
return [
|
return [
|
||||||
id,
|
id,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ import type { SubBlockConfig } from '@/blocks/types'
|
|||||||
import { isAnnotationOnlyBlock } from '@/executor/constants'
|
import { isAnnotationOnlyBlock } from '@/executor/constants'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import {
|
import { getUniqueBlockName, mergeSubblockState, normalizeName } from '@/stores/workflows/utils'
|
||||||
getUniqueBlockName,
|
|
||||||
mergeSubblockState,
|
|
||||||
normalizeBlockName,
|
|
||||||
} from '@/stores/workflows/utils'
|
|
||||||
import type {
|
import type {
|
||||||
Position,
|
Position,
|
||||||
SubBlockState,
|
SubBlockState,
|
||||||
@@ -676,7 +672,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
if (!oldBlock) return { success: false, changedSubblocks: [] }
|
if (!oldBlock) return { success: false, changedSubblocks: [] }
|
||||||
|
|
||||||
// Check for normalized name collisions
|
// Check for normalized name collisions
|
||||||
const normalizedNewName = normalizeBlockName(name)
|
const normalizedNewName = normalizeName(name)
|
||||||
const currentBlocks = get().blocks
|
const currentBlocks = get().blocks
|
||||||
|
|
||||||
// Find any other block with the same normalized name
|
// Find any other block with the same normalized name
|
||||||
@@ -684,7 +680,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
return (
|
return (
|
||||||
blockId !== id && // Different block
|
blockId !== id && // Different block
|
||||||
block.name && // Has a name
|
block.name && // Has a name
|
||||||
normalizeBlockName(block.name) === normalizedNewName // Same normalized name
|
normalizeName(block.name) === normalizedNewName // Same normalized name
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -75,16 +75,10 @@ export const notionCreateDatabaseTool: ToolConfig<NotionCreateDatabaseParams, No
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format parent ID
|
|
||||||
const formattedParentId = params.parentId.replace(
|
|
||||||
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
|
|
||||||
'$1-$2-$3-$4-$5'
|
|
||||||
)
|
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
parent: {
|
parent: {
|
||||||
type: 'page_id',
|
type: 'page_id',
|
||||||
page_id: formattedParentId,
|
page_id: params.parentId,
|
||||||
},
|
},
|
||||||
title: [
|
title: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -54,21 +54,13 @@ export const notionCreatePageTool: ToolConfig<NotionCreatePageParams, NotionResp
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
body: (params: NotionCreatePageParams) => {
|
body: (params: NotionCreatePageParams) => {
|
||||||
// Format parent ID with hyphens if needed
|
|
||||||
const formattedParentId = params.parentId.replace(
|
|
||||||
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
|
|
||||||
'$1-$2-$3-$4-$5'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Prepare the body for page parent
|
|
||||||
const body: any = {
|
const body: any = {
|
||||||
parent: {
|
parent: {
|
||||||
type: 'page_id',
|
type: 'page_id',
|
||||||
page_id: formattedParentId,
|
page_id: params.parentId,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add title if provided
|
|
||||||
if (params.title) {
|
if (params.title) {
|
||||||
body.properties = {
|
body.properties = {
|
||||||
title: {
|
title: {
|
||||||
@@ -87,7 +79,6 @@ export const notionCreatePageTool: ToolConfig<NotionCreatePageParams, NotionResp
|
|||||||
body.properties = {}
|
body.properties = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add content if provided
|
|
||||||
if (params.content) {
|
if (params.content) {
|
||||||
body.children = [
|
body.children = [
|
||||||
{
|
{
|
||||||
@@ -115,7 +106,6 @@ export const notionCreatePageTool: ToolConfig<NotionCreatePageParams, NotionResp
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
let pageTitle = 'Untitled'
|
let pageTitle = 'Untitled'
|
||||||
|
|
||||||
// Try to extract the title from properties
|
|
||||||
if (data.properties?.title) {
|
if (data.properties?.title) {
|
||||||
const titleProperty = data.properties.title
|
const titleProperty = data.properties.title
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -48,11 +48,7 @@ export const notionQueryDatabaseTool: ToolConfig<NotionQueryDatabaseParams, Noti
|
|||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params: NotionQueryDatabaseParams) => {
|
url: (params: NotionQueryDatabaseParams) => {
|
||||||
const formattedId = params.databaseId.replace(
|
return `https://api.notion.com/v1/databases/${params.databaseId}/query`
|
||||||
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
|
|
||||||
'$1-$2-$3-$4-$5'
|
|
||||||
)
|
|
||||||
return `https://api.notion.com/v1/databases/${formattedId}/query`
|
|
||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: (params: NotionQueryDatabaseParams) => {
|
headers: (params: NotionQueryDatabaseParams) => {
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ export const notionReadTool: ToolConfig<NotionReadParams, NotionResponse> = {
|
|||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params: NotionReadParams) => {
|
url: (params: NotionReadParams) => {
|
||||||
// Format page ID with hyphens if needed
|
return `https://api.notion.com/v1/pages/${params.pageId}`
|
||||||
const formattedId = params.pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5')
|
|
||||||
|
|
||||||
// Use the page endpoint to get page properties
|
|
||||||
return `https://api.notion.com/v1/pages/${formattedId}`
|
|
||||||
},
|
},
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: (params: NotionReadParams) => {
|
headers: (params: NotionReadParams) => {
|
||||||
@@ -85,12 +81,9 @@ export const notionReadTool: ToolConfig<NotionReadParams, NotionResponse> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format page ID for blocks endpoint
|
|
||||||
const formattedId = pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5')
|
|
||||||
|
|
||||||
// Fetch page content using blocks endpoint
|
// Fetch page content using blocks endpoint
|
||||||
const blocksResponse = await fetch(
|
const blocksResponse = await fetch(
|
||||||
`https://api.notion.com/v1/blocks/${formattedId}/children?page_size=100`,
|
`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -34,13 +34,7 @@ export const notionReadDatabaseTool: ToolConfig<NotionReadDatabaseParams, Notion
|
|||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params: NotionReadDatabaseParams) => {
|
url: (params: NotionReadDatabaseParams) => {
|
||||||
// Format database ID with hyphens if needed
|
return `https://api.notion.com/v1/databases/${params.databaseId}`
|
||||||
const formattedId = params.databaseId.replace(
|
|
||||||
/(.{8})(.{4})(.{4})(.{4})(.{12})/,
|
|
||||||
'$1-$2-$3-$4-$5'
|
|
||||||
)
|
|
||||||
|
|
||||||
return `https://api.notion.com/v1/databases/${formattedId}`
|
|
||||||
},
|
},
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: (params: NotionReadDatabaseParams) => {
|
headers: (params: NotionReadDatabaseParams) => {
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ export const notionUpdatePageTool: ToolConfig<NotionUpdatePageParams, NotionResp
|
|||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params: NotionUpdatePageParams) => {
|
url: (params: NotionUpdatePageParams) => {
|
||||||
// Format page ID with hyphens if needed
|
return `https://api.notion.com/v1/pages/${params.pageId}`
|
||||||
const formattedId = params.pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5')
|
|
||||||
return `https://api.notion.com/v1/pages/${formattedId}`
|
|
||||||
},
|
},
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: (params: NotionUpdatePageParams) => {
|
headers: (params: NotionUpdatePageParams) => {
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ export const notionWriteTool: ToolConfig<NotionWriteParams, NotionResponse> = {
|
|||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params: NotionWriteParams) => {
|
url: (params: NotionWriteParams) => {
|
||||||
// Format page ID with hyphens if needed
|
return `https://api.notion.com/v1/blocks/${params.pageId}/children`
|
||||||
const formattedId = params.pageId.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5')
|
|
||||||
return `https://api.notion.com/v1/blocks/${formattedId}/children`
|
|
||||||
},
|
},
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: (params: NotionWriteParams) => {
|
headers: (params: NotionWriteParams) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user