Stashing condition and code changes before proceeding with final implementation

This commit is contained in:
Emir Karabeg
2025-02-05 18:48:01 -08:00
parent 429a98e96b
commit 3471520347
5 changed files with 268 additions and 60 deletions

View File

@@ -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<number[]>([])
// 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 (
<div
className={cn(
'font-mono text-sm border rounded-md overflow-visible relative',
!inConditionSubBlock && 'border',
'font-mono text-sm rounded-md overflow-visible relative',
'bg-background text-muted-foreground',
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
isConnecting && !inConditionSubBlock && 'ring-2 ring-blue-500 ring-offset-2'
)}
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
onDragOver={(e) => !inConditionSubBlock && e.preventDefault()}
onDrop={(e) => !inConditionSubBlock && handleDrop(e)}
>
{/* Updated line numbers */}
<div
@@ -194,27 +264,13 @@ export function Code({ blockId, subBlockId, isConnecting }: CodeProps) {
<div ref={editorRef} className="pl-[30px] pt-0 mt-0 relative">
{code.length === 0 && (
<div className="absolute left-[42px] top-[12px] text-muted-foreground/50 select-none pointer-events-none">
Write JavaScript...
{inConditionSubBlock ? '<response> === true' : 'Write JavaScript...'}
</div>
)}
<Editor
value={code}
onValueChange={(newCode) => {
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 && (
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={code}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
}}
/>
<div className="absolute left-0 right-0 top-full z-50">
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={code}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
onSourceBlockIdChange?.(null)
}}
/>
</div>
)}
</div>
</div>

View File

@@ -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<string | null>(null)
const [activeConditionId, setActiveConditionId] = useState<string | null>(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 (
<div className="space-y-4">
{conditions.map((condition) => (
<div key={condition.id} className="group flex flex-col w-full relative">
<div className="rounded-md border overflow-hidden">
<div className="flex items-center justify-between px-3 py-1.5 border-b bg-background">
<span className="text-sm font-medium text-muted-foreground">{condition.type}</span>
<div className="flex items-center gap-2">
{condition.type !== 'else' && (
<Button
variant="ghost"
size="sm"
onClick={() => addCondition(condition.id)}
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
>
<PlusIcon className="w-3 h-3 mr-1" />
Add
</Button>
)}
{condition.type !== 'if' && (
<button
onClick={() => removeCondition(condition.id)}
className="text-muted-foreground hover:text-foreground"
>
<XIcon className="w-4 h-4" />
</button>
)}
</div>
</div>
<Code
blockId={blockId}
subBlockId={condition.id}
isConnecting={isConnecting}
inConditionSubBlock={true}
value={condition.code}
onChange={(newCode) => handleCodeChange(condition.id, newCode)}
controlled={true}
onSourceBlockIdChange={setActiveSourceBlockId}
/>
</div>
{showTags && activeConditionId === condition.id && (
<div className="absolute left-0 right-0 top-full z-50">
<TagDropdown
visible={showTags}
onSelect={handleTagSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={condition.code}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
setActiveConditionId(null)
}}
/>
</div>
)}
</div>
))}
</div>
)
}

View File

@@ -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 (
<ConditionInput blockId={blockId} subBlockId={config.id} isConnecting={isConnecting} />
)
default:
return null
}

View File

@@ -28,16 +28,8 @@ export const ConditionBlock: BlockConfig<CodeExecutionOutput> = {
},
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,
},

View File

@@ -41,6 +41,7 @@ export type SubBlockType =
| 'switch'
| 'tool-input'
| 'checkbox-list'
| 'condition-input'
export type SubBlockLayout = 'full' | 'half'
export interface ParamConfig {