diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/filter-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/filter-format.tsx index 92230598b..be7698d8a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/filter-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/filter-format.tsx @@ -6,8 +6,10 @@ import { Button, Combobox, type ComboboxOption, Input } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { COMPARISON_OPERATORS, + conditionsToJsonString, type FilterCondition, generateFilterId, + jsonStringToConditions, LOGICAL_OPERATORS, } from '@/lib/table/filter-builder-utils' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' @@ -23,6 +25,10 @@ interface FilterFormatProps { disabled?: boolean columns?: Array<{ value: string; label: string }> tableIdSubBlockId?: string + /** SubBlock ID for the mode dropdown (e.g., 'builderMode' or 'bulkFilterMode') */ + modeSubBlockId?: string + /** SubBlock ID for the JSON filter (e.g., 'filter' or 'filterCriteria') */ + jsonSubBlockId?: string } /** @@ -44,14 +50,62 @@ export function FilterFormat({ disabled = false, columns: propColumns, tableIdSubBlockId = 'tableId', + modeSubBlockId, + jsonSubBlockId, }: FilterFormatProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [tableIdValue] = useSubBlockValue(blockId, tableIdSubBlockId) const [dynamicColumns, setDynamicColumns] = useState([]) const fetchedTableIdRef = useRef(null) + // For syncing with JSON editor mode + const [modeValue] = useSubBlockValue(blockId, modeSubBlockId || '_unused_mode') + const [jsonValue, setJsonValue] = useSubBlockValue( + blockId, + jsonSubBlockId || '_unused_json' + ) + const prevModeRef = useRef(null) + const isSyncingRef = useRef(false) + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) + // Sync from JSON when switching to builder mode + useEffect(() => { + if (!modeSubBlockId || !jsonSubBlockId || isPreview) return + + // Detect mode change to 'builder' + if ( + prevModeRef.current !== null && + prevModeRef.current !== 'builder' && + modeValue === 'builder' + ) { + // Switching from JSON to Builder - sync JSON to conditions + if (jsonValue && typeof jsonValue === 'string' && jsonValue.trim()) { + isSyncingRef.current = true + const conditions = jsonStringToConditions(jsonValue) + if (conditions.length > 0) { + setStoreValue(conditions) + } + isSyncingRef.current = false + } + } + prevModeRef.current = modeValue + }, [modeValue, jsonValue, modeSubBlockId, jsonSubBlockId, setStoreValue, isPreview]) + + // Sync to JSON when conditions change (and we're in builder mode) + useEffect(() => { + if (!modeSubBlockId || !jsonSubBlockId || isPreview || isSyncingRef.current) return + if (modeValue !== 'builder') return + + const conditions = Array.isArray(storeValue) ? storeValue : [] + if (conditions.length > 0) { + const jsonString = conditionsToJsonString(conditions) + if (jsonString !== jsonValue) { + setJsonValue(jsonString) + } + } + }, [storeValue, modeValue, modeSubBlockId, jsonSubBlockId, jsonValue, setJsonValue, isPreview]) + // Fetch columns when tableId changes useEffect(() => { const fetchColumns = async () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/sort-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/sort-format.tsx index 5e0c7c5ed..b4cda420b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/sort-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/sort-format.tsx @@ -5,8 +5,10 @@ import { Plus, X } from 'lucide-react' import { Button, Combobox, type ComboboxOption } from '@/components/emcn' import { generateSortId, + jsonStringToSortConditions, SORT_DIRECTIONS, type SortCondition, + sortConditionsToJsonString, } from '@/lib/table/filter-builder-utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' @@ -18,6 +20,10 @@ interface SortFormatProps { disabled?: boolean columns?: Array<{ value: string; label: string }> tableIdSubBlockId?: string + /** SubBlock ID for the mode dropdown (e.g., 'builderMode') */ + modeSubBlockId?: string + /** SubBlock ID for the JSON sort (e.g., 'sort') */ + jsonSubBlockId?: string } /** @@ -37,12 +43,60 @@ export function SortFormat({ disabled = false, columns: propColumns, tableIdSubBlockId = 'tableId', + modeSubBlockId, + jsonSubBlockId, }: SortFormatProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [tableIdValue] = useSubBlockValue(blockId, tableIdSubBlockId) const [dynamicColumns, setDynamicColumns] = useState([]) const fetchedTableIdRef = useRef(null) + // For syncing with JSON editor mode + const [modeValue] = useSubBlockValue(blockId, modeSubBlockId || '_unused_mode') + const [jsonValue, setJsonValue] = useSubBlockValue( + blockId, + jsonSubBlockId || '_unused_json' + ) + const prevModeRef = useRef(null) + const isSyncingRef = useRef(false) + + // Sync from JSON when switching to builder mode + useEffect(() => { + if (!modeSubBlockId || !jsonSubBlockId || isPreview) return + + // Detect mode change to 'builder' + if ( + prevModeRef.current !== null && + prevModeRef.current !== 'builder' && + modeValue === 'builder' + ) { + // Switching from JSON to Builder - sync JSON to conditions + if (jsonValue && typeof jsonValue === 'string' && jsonValue.trim()) { + isSyncingRef.current = true + const conditions = jsonStringToSortConditions(jsonValue) + if (conditions.length > 0) { + setStoreValue(conditions) + } + isSyncingRef.current = false + } + } + prevModeRef.current = modeValue + }, [modeValue, jsonValue, modeSubBlockId, jsonSubBlockId, setStoreValue, isPreview]) + + // Sync to JSON when conditions change (and we're in builder mode) + useEffect(() => { + if (!modeSubBlockId || !jsonSubBlockId || isPreview || isSyncingRef.current) return + if (modeValue !== 'builder') return + + const conditions = Array.isArray(storeValue) ? storeValue : [] + if (conditions.length > 0) { + const jsonString = sortConditionsToJsonString(conditions) + if (jsonString !== jsonValue) { + setJsonValue(jsonString) + } + } + }, [storeValue, modeValue, modeSubBlockId, jsonSubBlockId, jsonValue, setJsonValue, isPreview]) + // Fetch columns when tableId changes useEffect(() => { const fetchColumns = async () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx index e1c6dce71..1f08da159 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx @@ -1,5 +1,6 @@ -import { useRef } from 'react' +import { useRef, useState } from 'react' import { Plus } from 'lucide-react' +import { useParams } from 'next/navigation' import { Trash } from '@/components/emcn/icons/trash' import 'prismjs/components/prism-json' import Editor from 'react-simple-code-editor' @@ -17,11 +18,26 @@ import { } from '@/components/emcn' import { Label } from '@/components/ui/label' import { cn } from '@/lib/core/utils/cn' +import { + isLikelyReferenceSegment, + SYSTEM_REFERENCE_PREFIXES, + splitReferenceSegment, +} from '@/lib/workflows/sanitization/references' +import { + checkEnvVarTrigger, + EnvVarDropdown, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' -import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' +import { + checkTagTrigger, + TagDropdown, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' +import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation' +import { useTagSelection } from '@/hooks/kb/use-tag-selection' +import { normalizeName } from '@/stores/workflows/utils' interface Field { id: string @@ -81,6 +97,61 @@ const createDefaultField = (): Field => ({ */ const validateFieldName = (name: string): string => name.replace(/[\x00-\x1F"\\]/g, '').trim() +/** + * Placeholder type for code highlighting + */ +interface CodePlaceholder { + placeholder: string + original: string + type: 'var' | 'env' +} + +/** + * Creates a syntax highlighter function with custom reference and environment variable highlighting. + */ +const createHighlightFunction = ( + shouldHighlightReference: (part: string) => boolean +): ((codeToHighlight: string) => string) => { + return (codeToHighlight: string): string => { + const placeholders: CodePlaceholder[] = [] + let processedCode = codeToHighlight + + processedCode = processedCode.replace(createEnvVarPattern(), (match) => { + const placeholder = `__ENV_VAR_${placeholders.length}__` + placeholders.push({ placeholder, original: match, type: 'env' }) + return placeholder + }) + + processedCode = processedCode.replace(createReferencePattern(), (match) => { + if (shouldHighlightReference(match)) { + const placeholder = `__VAR_REF_${placeholders.length}__` + placeholders.push({ placeholder, original: match, type: 'var' }) + return placeholder + } + return match + }) + + let highlightedCode = highlight(processedCode, languages.json, 'json') + + placeholders.forEach(({ placeholder, original, type }) => { + if (type === 'env') { + highlightedCode = highlightedCode.replace( + placeholder, + `${original}` + ) + } else if (type === 'var') { + const escaped = original.replace(//g, '>') + highlightedCode = highlightedCode.replace( + placeholder, + `${escaped}` + ) + } + }) + + return highlightedCode + } +} + export function FieldFormat({ blockId, subBlockId, @@ -93,12 +164,67 @@ export function FieldFormat({ showValue = false, valuePlaceholder = 'Enter default value', }: FieldFormatProps) { + const params = useParams() + const workspaceId = params.workspaceId as string + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const valueInputRefs = useRef>({}) const nameInputRefs = useRef>({}) const overlayRefs = useRef>({}) const nameOverlayRefs = useRef>({}) + const codeEditorRefs = useRef>({}) const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) + const emitTagSelection = useTagSelection(blockId, subBlockId) + + // State for code editor dropdowns (per field) + const [codeEditorDropdownState, setCodeEditorDropdownState] = useState< + Record< + string, + { + showTags: boolean + showEnvVars: boolean + searchTerm: string + cursorPosition: number + activeSourceBlockId: string | null + } + > + >({}) + + /** + * Determines whether a `<...>` segment should be highlighted as a reference. + */ + const shouldHighlightReference = (part: string): boolean => { + if (!part.startsWith('<') || !part.endsWith('>')) { + return false + } + + if (!isLikelyReferenceSegment(part)) { + return false + } + + const split = splitReferenceSegment(part) + if (!split) { + return false + } + + const reference = split.reference + + if (!accessiblePrefixes) { + return true + } + + const inner = reference.slice(1, -1) + const [prefix] = inner.split('.') + const normalizedPrefix = normalizeName(prefix) + + if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) { + return true + } + + return accessiblePrefixes.has(normalizedPrefix) + } + + const highlightCode = createHighlightFunction(shouldHighlightReference) const inputController = useSubBlockInput({ blockId, @@ -327,9 +453,25 @@ export function FieldFormat({ /> ) - if (field.type === 'object') { + // Code editor types with tag support + if (field.type === 'object' || field.type === 'array' || field.type === 'files') { const lineCount = fieldValue.split('\n').length const gutterWidth = calculateGutterWidth(lineCount) + const editorFieldKey = `code-${field.id}` + const dropdownState = codeEditorDropdownState[editorFieldKey] || { + showTags: false, + showEnvVars: false, + searchTerm: '', + cursorPosition: 0, + activeSourceBlockId: null, + } + + const placeholders: Record = { + object: '{\n "key": "value"\n}', + array: '[\n 1, 2, 3\n]', + files: + '[\n {\n "data": "",\n "type": "file",\n "name": "document.pdf",\n "mime": "application/pdf"\n }\n]', + } const renderLineNumbers = () => { return Array.from({ length: lineCount }, (_, i) => ( @@ -343,106 +485,189 @@ export function FieldFormat({ )) } - return ( - - {renderLineNumbers()} - - - {'{\n "key": "value"\n}'} - - { - if (!isReadOnly) { - updateField(field.id, 'value', newValue) - } - }} - highlight={(code) => highlight(code, languages.json, 'json')} - disabled={isReadOnly} - {...getCodeEditorProps({ disabled: isReadOnly })} - /> - - - ) - } + const handleCodeChange = (newValue: string) => { + if (isReadOnly) return + updateField(field.id, 'value', newValue) - if (field.type === 'array') { - const lineCount = fieldValue.split('\n').length - const gutterWidth = calculateGutterWidth(lineCount) + const editorContainer = codeEditorRefs.current[editorFieldKey] + const textarea = editorContainer?.querySelector('textarea') + if (textarea) { + const pos = textarea.selectionStart + const tagTrigger = checkTagTrigger(newValue, pos) + const envVarTrigger = checkEnvVarTrigger(newValue, pos) - const renderLineNumbers = () => { - return Array.from({ length: lineCount }, (_, i) => ( -
- {i + 1} -
- )) + setCodeEditorDropdownState((prev) => ({ + ...prev, + [editorFieldKey]: { + showTags: tagTrigger.show, + showEnvVars: envVarTrigger.show, + searchTerm: envVarTrigger.show ? envVarTrigger.searchTerm : '', + cursorPosition: pos, + activeSourceBlockId: tagTrigger.show ? dropdownState.activeSourceBlockId : null, + }, + })) + } + } + + const handleTagSelect = (newValue: string) => { + if (!isReadOnly) { + updateField(field.id, 'value', newValue) + emitTagSelection(newValue) + } + setCodeEditorDropdownState((prev) => ({ + ...prev, + [editorFieldKey]: { + ...dropdownState, + showTags: false, + activeSourceBlockId: null, + }, + })) + setTimeout(() => { + codeEditorRefs.current[editorFieldKey]?.querySelector('textarea')?.focus() + }, 0) + } + + const handleEnvVarSelect = (newValue: string) => { + if (!isReadOnly) { + updateField(field.id, 'value', newValue) + emitTagSelection(newValue) + } + setCodeEditorDropdownState((prev) => ({ + ...prev, + [editorFieldKey]: { + ...dropdownState, + showEnvVars: false, + searchTerm: '', + }, + })) + setTimeout(() => { + codeEditorRefs.current[editorFieldKey]?.querySelector('textarea')?.focus() + }, 0) + } + + const handleDrop = (e: React.DragEvent) => { + if (isReadOnly) return + e.preventDefault() + try { + const data = JSON.parse(e.dataTransfer.getData('application/json')) + if (data.type !== 'connectionBlock') return + + const textarea = codeEditorRefs.current[editorFieldKey]?.querySelector('textarea') + const dropPosition = textarea?.selectionStart ?? fieldValue.length + const newValue = `${fieldValue.slice(0, dropPosition)}<${fieldValue.slice(dropPosition)}` + + updateField(field.id, 'value', newValue) + const newCursorPosition = dropPosition + 1 + + setTimeout(() => { + if (textarea) { + textarea.focus() + textarea.selectionStart = newCursorPosition + textarea.selectionEnd = newCursorPosition + + setCodeEditorDropdownState((prev) => ({ + ...prev, + [editorFieldKey]: { + showTags: true, + showEnvVars: false, + searchTerm: '', + cursorPosition: newCursorPosition, + activeSourceBlockId: data.connectionData?.sourceBlockId || null, + }, + })) + } + }, 0) + } catch { + // Ignore drop errors + } } return ( - - {renderLineNumbers()} - - - {'[\n 1, 2, 3\n]'} - - { - if (!isReadOnly) { - updateField(field.id, 'value', newValue) - } - }} - highlight={(code) => highlight(code, languages.json, 'json')} - disabled={isReadOnly} - {...getCodeEditorProps({ disabled: isReadOnly })} - /> - - - ) - } - - if (field.type === 'files') { - const lineCount = fieldValue.split('\n').length - const gutterWidth = calculateGutterWidth(lineCount) - - const renderLineNumbers = () => { - return Array.from({ length: lineCount }, (_, i) => ( -
- {i + 1} -
- )) - } - - return ( - - {renderLineNumbers()} - - - { - '[\n {\n "data": "",\n "type": "file",\n "name": "document.pdf",\n "mime": "application/pdf"\n }\n]' - } - - { - if (!isReadOnly) { - updateField(field.id, 'value', newValue) - } - }} - highlight={(code) => highlight(code, languages.json, 'json')} - disabled={isReadOnly} - {...getCodeEditorProps({ disabled: isReadOnly })} - /> - - +
{ + if (el) codeEditorRefs.current[editorFieldKey] = el + }} + onDragOver={(e) => e.preventDefault()} + onDrop={handleDrop} + > + + {renderLineNumbers()} + + + {placeholders[field.type]} + + { + if (e.key === 'Escape') { + setCodeEditorDropdownState((prev) => ({ + ...prev, + [editorFieldKey]: { + ...dropdownState, + showTags: false, + showEnvVars: false, + }, + })) + } + }} + highlight={highlightCode} + disabled={isReadOnly} + {...getCodeEditorProps({ disabled: isReadOnly })} + /> + {dropdownState.showEnvVars && !isReadOnly && ( + { + setCodeEditorDropdownState((prev) => ({ + ...prev, + [editorFieldKey]: { + ...dropdownState, + showEnvVars: false, + searchTerm: '', + }, + })) + }} + inputRef={{ + current: codeEditorRefs.current[editorFieldKey]?.querySelector( + 'textarea' + ) as HTMLTextAreaElement, + }} + /> + )} + {dropdownState.showTags && !isReadOnly && ( + { + setCodeEditorDropdownState((prev) => ({ + ...prev, + [editorFieldKey]: { + ...dropdownState, + showTags: false, + activeSourceBlockId: null, + }, + })) + }} + inputRef={{ + current: codeEditorRefs.current[editorFieldKey]?.querySelector( + 'textarea' + ) as HTMLTextAreaElement, + }} + /> + )} + + +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index e869d1627..68fbe6953 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -1,5 +1,6 @@ -import { type JSX, type MouseEvent, memo, useRef, useState } from 'react' -import { AlertTriangle, Wand2 } from 'lucide-react' +import { type JSX, type MouseEvent, memo, useCallback, useRef, useState } from 'react' +import { AlertTriangle, ExternalLink, Wand2 } from 'lucide-react' +import { useParams } from 'next/navigation' import { Label, Tooltip } from '@/components/emcn/components' import { Button } from '@/components/ui/button' import { cn } from '@/lib/core/utils/cn' @@ -46,6 +47,8 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import type { SubBlockConfig } from '@/blocks/types' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' /** * Interface for wand control handlers exposed by sub-block inputs @@ -293,6 +296,99 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool ) } +/** + * Props for the DropdownWithTableLink component + */ +interface DropdownWithTableLinkProps { + blockId: string + config: SubBlockConfig + isPreview: boolean + previewValue: string | string[] | null | undefined + isDisabled: boolean + handleMouseDown: (e: MouseEvent) => void +} + +/** + * Renders a dropdown with an optional navigation link for table selectors. + * When the dropdown is for selecting a table (tableId), shows an icon button + * to navigate directly to the table page view. + */ +function DropdownWithTableLink({ + blockId, + config, + isPreview, + previewValue, + isDisabled, + handleMouseDown, +}: DropdownWithTableLinkProps): JSX.Element { + const params = useParams() + const workspaceId = params.workspaceId as string + + const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) + const tableId = useSubBlockStore( + useCallback( + (state) => { + if (!activeWorkflowId) return null + const value = state.workflowValues[activeWorkflowId]?.[blockId]?.[config.id] + return typeof value === 'string' ? value : null + }, + [activeWorkflowId, blockId, config.id] + ) + ) + + const isTableSelector = config.id === 'tableId' + const hasSelectedTable = isTableSelector && tableId && !tableId.startsWith('<') + + const handleNavigateToTable = useCallback( + (e: MouseEvent) => { + e.stopPropagation() + if (tableId && workspaceId) { + window.open(`/workspace/${workspaceId}/tables/${tableId}`, '_blank') + } + }, + [workspaceId, tableId] + ) + + return ( +
+
+ +
+ {hasSelectedTable && !isPreview && ( + + + + + +

View table

+
+
+ )} +
+ ) +} + /** * Renders a single workflow sub-block input based on config.type. * @@ -451,23 +547,14 @@ function SubBlockComponent({ case 'dropdown': return ( -
- -
+ ) case 'combobox': @@ -800,7 +887,17 @@ function SubBlockComponent({ /> ) - case 'filter-format': + case 'filter-format': { + // Determine sync props based on subBlockId + let modeSubBlockId: string | undefined + let jsonSubBlockId: string | undefined + if (config.id === 'filterBuilder') { + modeSubBlockId = 'builderMode' + jsonSubBlockId = 'filter' + } else if (config.id === 'bulkFilterBuilder') { + modeSubBlockId = 'bulkFilterMode' + jsonSubBlockId = 'filterCriteria' + } return ( ) + } - case 'sort-format': + case 'sort-format': { + // Determine sync props based on subBlockId + let modeSubBlockId: string | undefined + let jsonSubBlockId: string | undefined + if (config.id === 'sortBuilder') { + modeSubBlockId = 'builderMode' + jsonSubBlockId = 'sort' + } return ( ) + } case 'channel-selector': case 'user-selector': diff --git a/apps/sim/blocks/blocks/table.ts b/apps/sim/blocks/blocks/table.ts index b3266a295..de24790ce 100644 --- a/apps/sim/blocks/blocks/table.ts +++ b/apps/sim/blocks/blocks/table.ts @@ -469,7 +469,8 @@ Return ONLY the sort JSON:`, const { operation, ...rest } = params /** - * Helper to parse JSON with better error messages + * Helper to parse JSON with better error messages. + * Also handles common issues with block references in JSON. */ const parseJSON = (value: string | any, fieldName: string): any => { if (typeof value !== 'string') return value @@ -478,9 +479,22 @@ Return ONLY the sort JSON:`, return JSON.parse(value) } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) - throw new Error( - `Invalid JSON in ${fieldName}: ${errorMsg}. Make sure all property names are in double quotes (e.g., {"name": "value"} not {name: "value"})` + + // Check if the error might be due to unquoted string values (common when block references are resolved) + // This happens when users write {"field": } instead of {"field": ""} + const unquotedValueMatch = value.match( + /:\s*([a-zA-Z][a-zA-Z0-9_\s]*[a-zA-Z0-9]|[a-zA-Z])\s*[,}]/ ) + + let hint = + 'Make sure all property names are in double quotes (e.g., {"name": "value"} not {name: "value"}).' + + if (unquotedValueMatch) { + hint = + 'It looks like a string value is not quoted. When using block references in JSON, wrap them in double quotes: {"field": ""} not {"field": }.' + } + + throw new Error(`Invalid JSON in ${fieldName}: ${errorMsg}. ${hint}`) } }