mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
Added a dropdown selector for envvars to enhance UX for short-input and long-input sub-blocks
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { SubBlockConfig } from '@/blocks/types'
|
import { SubBlockConfig } from '@/blocks/types'
|
||||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||||
|
import { EnvVarDropdown, checkEnvVarTrigger } from '@/components/ui/env-var-dropdown'
|
||||||
|
|
||||||
interface LongInputProps {
|
interface LongInputProps {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -21,9 +22,23 @@ export function LongInput({
|
|||||||
config,
|
config,
|
||||||
}: LongInputProps) {
|
}: LongInputProps) {
|
||||||
const [value, setValue] = useSubBlockValue(blockId, subBlockId)
|
const [value, setValue] = useSubBlockValue(blockId, subBlockId)
|
||||||
|
const [showEnvVars, setShowEnvVars] = useState(false)
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [cursorPosition, setCursorPosition] = useState(0)
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const overlayRef = useRef<HTMLDivElement>(null)
|
const overlayRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Handle input changes
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newValue = e.target.value
|
||||||
|
const newCursorPosition = e.target.selectionStart ?? 0
|
||||||
|
setValue(newValue)
|
||||||
|
setCursorPosition(newCursorPosition)
|
||||||
|
const { show, searchTerm } = checkEnvVarTrigger(newValue, newCursorPosition)
|
||||||
|
setShowEnvVars(show)
|
||||||
|
setSearchTerm(searchTerm)
|
||||||
|
}
|
||||||
|
|
||||||
// Sync scroll position between textarea and overlay
|
// Sync scroll position between textarea and overlay
|
||||||
const handleScroll = (e: React.UIEvent<HTMLTextAreaElement>) => {
|
const handleScroll = (e: React.UIEvent<HTMLTextAreaElement>) => {
|
||||||
if (overlayRef.current) {
|
if (overlayRef.current) {
|
||||||
@@ -64,6 +79,13 @@ export function LongInput({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle key combinations
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setShowEnvVars(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -77,10 +99,15 @@ export function LongInput({
|
|||||||
rows={4}
|
rows={4}
|
||||||
placeholder={placeholder ?? ''}
|
placeholder={placeholder ?? ''}
|
||||||
value={value?.toString() ?? ''}
|
value={value?.toString() ?? ''}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={handleChange}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={() => {
|
||||||
|
setShowEnvVars(false)
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={overlayRef}
|
ref={overlayRef}
|
||||||
@@ -88,6 +115,17 @@ export function LongInput({
|
|||||||
>
|
>
|
||||||
{formatDisplayText(value?.toString() ?? '')}
|
{formatDisplayText(value?.toString() ?? '')}
|
||||||
</div>
|
</div>
|
||||||
|
<EnvVarDropdown
|
||||||
|
visible={showEnvVars}
|
||||||
|
onSelect={setValue}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
inputValue={value?.toString() ?? ''}
|
||||||
|
cursorPosition={cursorPosition}
|
||||||
|
onClose={() => {
|
||||||
|
setShowEnvVars(false)
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { SubBlockConfig } from '@/blocks/types'
|
import { SubBlockConfig } from '@/blocks/types'
|
||||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||||
|
import { EnvVarDropdown, checkEnvVarTrigger } from '@/components/ui/env-var-dropdown'
|
||||||
|
|
||||||
interface ShortInputProps {
|
interface ShortInputProps {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -23,10 +24,24 @@ export function ShortInput({
|
|||||||
config,
|
config,
|
||||||
}: ShortInputProps) {
|
}: ShortInputProps) {
|
||||||
const [isFocused, setIsFocused] = useState(false)
|
const [isFocused, setIsFocused] = useState(false)
|
||||||
|
const [showEnvVars, setShowEnvVars] = useState(false)
|
||||||
const [value, setValue] = useSubBlockValue(blockId, subBlockId)
|
const [value, setValue] = useSubBlockValue(blockId, subBlockId)
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [cursorPosition, setCursorPosition] = useState(0)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const overlayRef = useRef<HTMLDivElement>(null)
|
const overlayRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Handle input changes
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = e.target.value
|
||||||
|
const newCursorPosition = e.target.selectionStart ?? 0
|
||||||
|
setValue(newValue)
|
||||||
|
setCursorPosition(newCursorPosition)
|
||||||
|
const { show, searchTerm } = checkEnvVarTrigger(newValue, newCursorPosition)
|
||||||
|
setShowEnvVars(show)
|
||||||
|
setSearchTerm(searchTerm)
|
||||||
|
}
|
||||||
|
|
||||||
// Sync scroll position between input and overlay
|
// Sync scroll position between input and overlay
|
||||||
const handleScroll = (e: React.UIEvent<HTMLInputElement>) => {
|
const handleScroll = (e: React.UIEvent<HTMLInputElement>) => {
|
||||||
if (overlayRef.current) {
|
if (overlayRef.current) {
|
||||||
@@ -76,7 +91,14 @@ export function ShortInput({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLInputElement>) => {
|
const handleDragOver = (e: React.DragEvent<HTMLInputElement>) => {
|
||||||
e.preventDefault() // This is needed to allow drops
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle key combinations
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setShowEnvVars(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value display logic
|
// Value display logic
|
||||||
@@ -98,12 +120,17 @@ export function ShortInput({
|
|||||||
placeholder={placeholder ?? ''}
|
placeholder={placeholder ?? ''}
|
||||||
type="text"
|
type="text"
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={handleChange}
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => {
|
||||||
|
setIsFocused(true)
|
||||||
|
setShowEnvVars(false)
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -114,6 +141,17 @@ export function ShortInput({
|
|||||||
? '•'.repeat(value?.toString().length ?? 0)
|
? '•'.repeat(value?.toString().length ?? 0)
|
||||||
: formatDisplayText(value?.toString() ?? '')}
|
: formatDisplayText(value?.toString() ?? '')}
|
||||||
</div>
|
</div>
|
||||||
|
<EnvVarDropdown
|
||||||
|
visible={showEnvVars}
|
||||||
|
onSelect={setValue}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
inputValue={value?.toString() ?? ''}
|
||||||
|
cursorPosition={cursorPosition}
|
||||||
|
onClose={() => {
|
||||||
|
setShowEnvVars(false)
|
||||||
|
setSearchTerm('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
137
components/ui/env-var-dropdown.tsx
Normal file
137
components/ui/env-var-dropdown.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useEnvironmentStore } from '@/stores/environment/store'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface EnvVarDropdownProps {
|
||||||
|
visible: boolean
|
||||||
|
onSelect: (newValue: string) => void
|
||||||
|
searchTerm?: string
|
||||||
|
className?: string
|
||||||
|
inputValue: string
|
||||||
|
cursorPosition: number
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnvVarDropdown: React.FC<EnvVarDropdownProps> = ({
|
||||||
|
visible,
|
||||||
|
onSelect,
|
||||||
|
searchTerm = '',
|
||||||
|
className,
|
||||||
|
inputValue,
|
||||||
|
cursorPosition,
|
||||||
|
onClose
|
||||||
|
}) => {
|
||||||
|
const envVars = useEnvironmentStore((state) => Object.keys(state.variables))
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
|
|
||||||
|
// Filter env vars based on search term
|
||||||
|
const filteredEnvVars = envVars.filter(envVar =>
|
||||||
|
envVar.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reset selection when filtered results change
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIndex(0)
|
||||||
|
}, [searchTerm])
|
||||||
|
|
||||||
|
// Handle environment variable selection
|
||||||
|
const handleEnvVarSelect = (envVar: string) => {
|
||||||
|
const textBeforeCursor = inputValue.slice(0, cursorPosition)
|
||||||
|
const textAfterCursor = inputValue.slice(cursorPosition)
|
||||||
|
|
||||||
|
// Find the position of the last '{{' before cursor
|
||||||
|
const lastOpenBraces = textBeforeCursor.lastIndexOf('{{')
|
||||||
|
if (lastOpenBraces === -1) return
|
||||||
|
|
||||||
|
const newValue = textBeforeCursor.slice(0, lastOpenBraces) +
|
||||||
|
'{{' + envVar + '}}' +
|
||||||
|
textAfterCursor
|
||||||
|
|
||||||
|
onSelect(newValue)
|
||||||
|
onClose?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle keyboard navigation
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!visible || filteredEnvVars.length === 0) return
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedIndex(prev =>
|
||||||
|
prev < filteredEnvVars.length - 1 ? prev + 1 : prev
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedIndex(prev => prev > 0 ? prev - 1 : prev)
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
e.preventDefault()
|
||||||
|
handleEnvVarSelect(filteredEnvVars[selectedIndex])
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault()
|
||||||
|
onClose?.()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add and remove keyboard event listener
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}
|
||||||
|
}, [visible, selectedIndex, filteredEnvVars])
|
||||||
|
|
||||||
|
if (!visible) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute z-[9999] w-full mt-1 overflow-hidden bg-popover rounded-md border shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{filteredEnvVars.length === 0 ? (
|
||||||
|
<div className="px-3 py-2 text-sm text-muted-foreground">
|
||||||
|
No matching environment variables
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-1">
|
||||||
|
{filteredEnvVars.map((envVar, index) => (
|
||||||
|
<button
|
||||||
|
key={envVar}
|
||||||
|
className={cn(
|
||||||
|
"w-full px-3 py-1.5 text-sm text-left",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
"focus:bg-accent focus:text-accent-foreground focus:outline-none",
|
||||||
|
index === selectedIndex && "bg-accent text-accent-foreground"
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault() // Prevent input blur
|
||||||
|
handleEnvVarSelect(envVar)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{envVar}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check for '{{' trigger and get search term
|
||||||
|
export const checkEnvVarTrigger = (text: string, cursorPosition: number): { show: boolean; searchTerm: string } => {
|
||||||
|
if (cursorPosition >= 2) {
|
||||||
|
const textBeforeCursor = text.slice(0, cursorPosition)
|
||||||
|
const match = textBeforeCursor.match(/\{\{(\w*)$/)
|
||||||
|
if (match) {
|
||||||
|
return { show: true, searchTerm: match[1] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { show: false, searchTerm: '' }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user