From 34715203470932fbb883a47a6bc755aa1c6746c9 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Wed, 5 Feb 2025 18:48:01 -0800 Subject: [PATCH] Stashing condition and code changes before proceeding with final implementation --- .../components/sub-block/components/code.tsx | 159 ++++++++++++------ .../sub-block/components/condition-input.tsx | 151 +++++++++++++++++ .../components/sub-block/sub-block.tsx | 5 + blocks/blocks/condition.ts | 12 +- blocks/types.ts | 1 + 5 files changed, 268 insertions(+), 60 deletions(-) create mode 100644 app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/code.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/code.tsx index 31498f784..d9157298b 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/code.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/code.tsx @@ -11,9 +11,23 @@ interface CodeProps { blockId: string subBlockId: string isConnecting: boolean + inConditionSubBlock?: boolean + value?: string + onChange?: (value: string) => void + controlled?: boolean + onSourceBlockIdChange?: (blockId: string | null) => void } -export function Code({ blockId, subBlockId, isConnecting }: CodeProps) { +export function Code({ + blockId, + subBlockId, + isConnecting, + inConditionSubBlock, + value: controlledValue, + onChange, + controlled = false, + onSourceBlockIdChange, +}: CodeProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [code, setCode] = useState('') const [lineCount, setLineCount] = useState(1) @@ -25,12 +39,14 @@ export function Code({ blockId, subBlockId, isConnecting }: CodeProps) { // Add new state for tracking visual line heights const [visualLineHeights, setVisualLineHeights] = useState([]) - // Sync code with store value on initial load and when store value changes + // Modify the useEffect to handle both controlled and uncontrolled modes useEffect(() => { - if (storeValue !== null) { + if (controlled) { + setCode(controlledValue || '') + } else if (storeValue !== null) { setCode(storeValue.toString()) } - }, [storeValue]) + }, [storeValue, controlledValue, controlled]) // Update the line counting logic to account for wrapped lines useEffect(() => { @@ -129,30 +145,91 @@ export function Code({ blockId, subBlockId, isConnecting }: CodeProps) { return numbers } - // Handle drops from connection blocks + const handleCodeChange = (newCode: string) => { + setCode(newCode) + if (controlled) { + onChange?.(newCode) + } else { + setStoreValue(newCode) + } + + // Get the textarea element + const textarea = editorRef.current?.querySelector('textarea') + if (textarea) { + // Important: Use requestAnimationFrame to ensure we get the updated cursor position + requestAnimationFrame(() => { + const pos = textarea.selectionStart + setCursorPosition(pos) + + const trigger = checkTagTrigger(newCode, pos) + setShowTags(trigger.show) + if (!trigger.show) { + setActiveSourceBlockId(null) + onSourceBlockIdChange?.(null) + } + }) + } + } + + // Add an onKeyDown handler to ensure we catch the '<' character immediately + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === '<') { + const textarea = e.target as HTMLTextAreaElement + const pos = textarea.selectionStart + const newCode = code.slice(0, pos) + '<' + code.slice(pos) + + setCode(newCode) + if (controlled) { + onChange?.(newCode) + } else { + setStoreValue(newCode) + } + + setCursorPosition(pos + 1) + setShowTags(true) + } + } + + // Handle tag selection + const handleTagSelect = (newValue: string) => { + setCode(newValue) + if (controlled) { + onChange?.(newValue) + } else { + setStoreValue(newValue) + } + setShowTags(false) + setActiveSourceBlockId(null) + onSourceBlockIdChange?.(null) + } + + // Modify handleDrop to support both controlled and uncontrolled modes const handleDrop = (e: React.DragEvent) => { e.preventDefault() try { const data = JSON.parse(e.dataTransfer.getData('application/json')) if (data.type !== 'connectionBlock') return - // Get current cursor position from the textarea const textarea = editorRef.current?.querySelector('textarea') const dropPosition = textarea?.selectionStart ?? code.length - // Insert '<' at drop position to trigger the dropdown const newValue = code.slice(0, dropPosition) + '<' + code.slice(dropPosition) setCode(newValue) - setStoreValue(newValue) + if (controlled) { + onChange?.(newValue) + } else { + setStoreValue(newValue) + } + setCursorPosition(dropPosition + 1) setShowTags(true) if (data.connectionData?.sourceBlockId) { setActiveSourceBlockId(data.connectionData.sourceBlockId) + onSourceBlockIdChange?.(data.connectionData.sourceBlockId) } - // Set cursor position after state updates setTimeout(() => { if (textarea) { textarea.selectionStart = dropPosition + 1 @@ -165,23 +242,16 @@ export function Code({ blockId, subBlockId, isConnecting }: CodeProps) { } } - // Handle tag selection - const handleTagSelect = (newValue: string) => { - setCode(newValue) - setStoreValue(newValue) - setShowTags(false) - setActiveSourceBlockId(null) - } - return (
e.preventDefault()} - onDrop={handleDrop} + onDragOver={(e) => !inConditionSubBlock && e.preventDefault()} + onDrop={(e) => !inConditionSubBlock && handleDrop(e)} > {/* Updated line numbers */}
{code.length === 0 && (
- Write JavaScript... + {inConditionSubBlock ? ' === true' : 'Write JavaScript...'}
)} { - setCode(newCode) - setStoreValue(newCode) - - // Check for tag trigger - const textarea = editorRef.current?.querySelector('textarea') - if (textarea) { - const pos = textarea.selectionStart - setCursorPosition(pos) - const trigger = checkTagTrigger(newCode, pos) - setShowTags(trigger.show) - if (!trigger.show) { - setActiveSourceBlockId(null) - } - } - }} + onValueChange={handleCodeChange} + onKeyDown={handleKeyDown} highlight={(code) => highlight(code, languages.javascript, 'javascript')} padding={12} style={{ @@ -227,18 +283,21 @@ export function Code({ blockId, subBlockId, isConnecting }: CodeProps) { /> {showTags && ( - { - setShowTags(false) - setActiveSourceBlockId(null) - }} - /> +
+ { + setShowTags(false) + setActiveSourceBlockId(null) + onSourceBlockIdChange?.(null) + }} + /> +
)}
diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx new file mode 100644 index 000000000..35297eac7 --- /dev/null +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx @@ -0,0 +1,151 @@ +import { useState } from 'react' +import { PlusIcon, XIcon } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { TagDropdown, checkTagTrigger } from '@/components/ui/tag-dropdown' +import { cn } from '@/lib/utils' +import { useSubBlockValue } from '../hooks/use-sub-block-value' +import { Code } from './code' + +interface ConditionInputProps { + blockId: string + subBlockId: string + isConnecting: boolean +} + +interface Condition { + id: string + type: 'if' | 'else if' | 'else' + code: string +} + +export function ConditionInput({ blockId, subBlockId, isConnecting }: ConditionInputProps) { + const [value, setValue] = useSubBlockValue(blockId, subBlockId) + const [showTags, setShowTags] = useState(false) + const [cursorPosition, setCursorPosition] = useState(0) + const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) + const [activeConditionId, setActiveConditionId] = useState(null) + + // Initialize with default if/else conditions if no value exists + const conditions: Condition[] = + Array.isArray(value) && value.length > 0 && 'type' in value[0] + ? (value as unknown as Condition[]) + : [ + { id: crypto.randomUUID(), type: 'if', code: '' }, + { id: crypto.randomUUID(), type: 'else', code: '' }, + ] + + const addCondition = (afterId: string) => { + const index = conditions.findIndex((c) => c.id === afterId) + const newCondition: Condition = { + id: crypto.randomUUID(), + type: 'else if', + code: '', + } + const newConditions = [ + ...conditions.slice(0, index + 1), + newCondition, + ...conditions.slice(index + 1), + ] + setValue(newConditions) + } + + const removeCondition = (id: string) => { + setValue(conditions.filter((c) => c.id !== id)) + } + + const updateCode = (id: string, code: string) => { + setValue(conditions.map((c) => (c.id === id ? { ...c, code } : c))) + } + + // Handle tag selection + const handleTagSelect = (newValue: string) => { + if (activeConditionId) { + const condition = conditions.find((c) => c.id === activeConditionId) + if (condition) { + updateCode(activeConditionId, newValue) + } + } + setShowTags(false) + setActiveSourceBlockId(null) + setActiveConditionId(null) + } + + // Handle code changes and tag triggers + const handleCodeChange = (conditionId: string, newCode: string) => { + updateCode(conditionId, newCode) + + // Check for tag trigger + const trigger = checkTagTrigger(newCode, cursorPosition) + if (trigger.show) { + setShowTags(true) + setActiveConditionId(conditionId) + } else { + setShowTags(false) + setActiveSourceBlockId(null) + setActiveConditionId(null) + } + } + + return ( +
+ {conditions.map((condition) => ( +
+
+
+ {condition.type} +
+ {condition.type !== 'else' && ( + + )} + {condition.type !== 'if' && ( + + )} +
+
+ handleCodeChange(condition.id, newCode)} + controlled={true} + onSourceBlockIdChange={setActiveSourceBlockId} + /> +
+ + {showTags && activeConditionId === condition.id && ( +
+ { + setShowTags(false) + setActiveSourceBlockId(null) + setActiveConditionId(null) + }} + /> +
+ )} +
+ ))} +
+ ) +} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index adb74f6fd..3bd7e12dc 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -2,6 +2,7 @@ import { Label } from '@/components/ui/label' import { SubBlockConfig } from '../../../../../../../blocks/types' import { CheckboxList } from './components/checkbox-list' import { Code } from './components/code' +import { ConditionInput } from './components/condition-input' import { Dropdown } from './components/dropdown' import { LongInput } from './components/long-input' import { ShortInput } from './components/short-input' @@ -82,6 +83,10 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) { layout={config.layout} /> ) + case 'condition-input': + return ( + + ) default: return null } diff --git a/blocks/blocks/condition.ts b/blocks/blocks/condition.ts index 802d318b0..0f50f163a 100644 --- a/blocks/blocks/condition.ts +++ b/blocks/blocks/condition.ts @@ -28,16 +28,8 @@ export const ConditionBlock: BlockConfig = { }, subBlocks: [ { - id: 'if', - title: 'if', - type: 'code', - layout: 'full', - outputHandle: true, - }, - { - id: 'elseIf', - title: 'else if', - type: 'code', + id: 'conditions', + type: 'condition-input', layout: 'full', outputHandle: true, }, diff --git a/blocks/types.ts b/blocks/types.ts index a9957a088..b05d2d6d9 100644 --- a/blocks/types.ts +++ b/blocks/types.ts @@ -41,6 +41,7 @@ export type SubBlockType = | 'switch' | 'tool-input' | 'checkbox-list' + | 'condition-input' export type SubBlockLayout = 'full' | 'half' export interface ParamConfig {