mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
Stashing condition and code changes before proceeding with final implementation
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ export type SubBlockType =
|
||||
| 'switch'
|
||||
| 'tool-input'
|
||||
| 'checkbox-list'
|
||||
| 'condition-input'
|
||||
export type SubBlockLayout = 'full' | 'half'
|
||||
|
||||
export interface ParamConfig {
|
||||
|
||||
Reference in New Issue
Block a user