Skip to content

Commit a3d46bc

Browse files
Sg312icecrasher321
authored andcommitted
improvement(router): add ports to router block (#2683)
* Add ports to router block * Add tag dropdowns * Fix lint * fix tests + add context into block preview --------- Co-authored-by: Vikhyath Mondreti <[email protected]>
1 parent 1eb1b8f commit a3d46bc

File tree

10 files changed

+863
-108
lines changed

10 files changed

+863
-108
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/condition-input/condition-input.tsx

Lines changed: 187 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getCodeEditorProps,
1313
highlight,
1414
languages,
15+
Textarea,
1516
Tooltip,
1617
} from '@/components/emcn'
1718
import { Trash } from '@/components/emcn/icons/trash'
@@ -74,6 +75,8 @@ interface ConditionInputProps {
7475
previewValue?: string | null
7576
/** Whether the component is disabled */
7677
disabled?: boolean
78+
/** Mode: 'condition' for code editor, 'router' for text input */
79+
mode?: 'condition' | 'router'
7780
}
7881

7982
/**
@@ -101,7 +104,9 @@ export function ConditionInput({
101104
isPreview = false,
102105
previewValue,
103106
disabled = false,
107+
mode = 'condition',
104108
}: ConditionInputProps) {
109+
const isRouterMode = mode === 'router'
105110
const params = useParams()
106111
const workspaceId = params.workspaceId as string
107112
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
@@ -161,32 +166,50 @@ export function ConditionInput({
161166
const shouldPersistRef = useRef<boolean>(false)
162167

163168
/**
164-
* Creates default if/else conditional blocks with stable IDs.
169+
* Creates default blocks with stable IDs.
170+
* For conditions: if/else blocks. For router: one route block.
165171
*
166-
* @returns Array of two default blocks (if and else)
172+
* @returns Array of default blocks
167173
*/
168-
const createDefaultBlocks = (): ConditionalBlock[] => [
169-
{
170-
id: generateStableId(blockId, 'if'),
171-
title: 'if',
172-
value: '',
173-
showTags: false,
174-
showEnvVars: false,
175-
searchTerm: '',
176-
cursorPosition: 0,
177-
activeSourceBlockId: null,
178-
},
179-
{
180-
id: generateStableId(blockId, 'else'),
181-
title: 'else',
182-
value: '',
183-
showTags: false,
184-
showEnvVars: false,
185-
searchTerm: '',
186-
cursorPosition: 0,
187-
activeSourceBlockId: null,
188-
},
189-
]
174+
const createDefaultBlocks = (): ConditionalBlock[] => {
175+
if (isRouterMode) {
176+
return [
177+
{
178+
id: generateStableId(blockId, 'route1'),
179+
title: 'route1',
180+
value: '',
181+
showTags: false,
182+
showEnvVars: false,
183+
searchTerm: '',
184+
cursorPosition: 0,
185+
activeSourceBlockId: null,
186+
},
187+
]
188+
}
189+
190+
return [
191+
{
192+
id: generateStableId(blockId, 'if'),
193+
title: 'if',
194+
value: '',
195+
showTags: false,
196+
showEnvVars: false,
197+
searchTerm: '',
198+
cursorPosition: 0,
199+
activeSourceBlockId: null,
200+
},
201+
{
202+
id: generateStableId(blockId, 'else'),
203+
title: 'else',
204+
value: '',
205+
showTags: false,
206+
showEnvVars: false,
207+
searchTerm: '',
208+
cursorPosition: 0,
209+
activeSourceBlockId: null,
210+
},
211+
]
212+
}
190213

191214
// Initialize with a loading state instead of default blocks
192215
const [conditionalBlocks, setConditionalBlocks] = useState<ConditionalBlock[]>([])
@@ -270,10 +293,13 @@ export function ConditionInput({
270293
const parsedBlocks = safeParseJSON(effectiveValueStr)
271294

272295
if (parsedBlocks) {
273-
const blocksWithCorrectTitles = parsedBlocks.map((block, index) => ({
274-
...block,
275-
title: index === 0 ? 'if' : index === parsedBlocks.length - 1 ? 'else' : 'else if',
276-
}))
296+
// For router mode, keep original titles. For condition mode, assign if/else if/else
297+
const blocksWithCorrectTitles = isRouterMode
298+
? parsedBlocks
299+
: parsedBlocks.map((block, index) => ({
300+
...block,
301+
title: index === 0 ? 'if' : index === parsedBlocks.length - 1 ? 'else' : 'else if',
302+
}))
277303

278304
setConditionalBlocks(blocksWithCorrectTitles)
279305
hasInitializedRef.current = true
@@ -573,12 +599,17 @@ export function ConditionInput({
573599

574600
/**
575601
* Updates block titles based on their position in the array.
576-
* First block is always 'if', last is 'else', middle ones are 'else if'.
602+
* For conditions: First block is 'if', last is 'else', middle ones are 'else if'.
603+
* For router: Titles are user-editable and not auto-updated.
577604
*
578605
* @param blocks - Array of conditional blocks
579606
* @returns Updated blocks with correct titles
580607
*/
581608
const updateBlockTitles = (blocks: ConditionalBlock[]): ConditionalBlock[] => {
609+
if (isRouterMode) {
610+
// For router mode, don't change titles - they're user-editable
611+
return blocks
612+
}
582613
return blocks.map((block, index) => ({
583614
...block,
584615
title: index === 0 ? 'if' : index === blocks.length - 1 ? 'else' : 'else if',
@@ -590,13 +621,15 @@ export function ConditionInput({
590621
if (isPreview || disabled) return
591622

592623
const blockIndex = conditionalBlocks.findIndex((block) => block.id === afterId)
593-
if (conditionalBlocks[blockIndex]?.title === 'else') return
624+
if (!isRouterMode && conditionalBlocks[blockIndex]?.title === 'else') return
594625

595-
const newBlockId = generateStableId(blockId, `else-if-${Date.now()}`)
626+
const newBlockId = isRouterMode
627+
? generateStableId(blockId, `route-${Date.now()}`)
628+
: generateStableId(blockId, `else-if-${Date.now()}`)
596629

597630
const newBlock: ConditionalBlock = {
598631
id: newBlockId,
599-
title: '',
632+
title: isRouterMode ? `route-${Date.now()}` : '',
600633
value: '',
601634
showTags: false,
602635
showEnvVars: false,
@@ -710,21 +743,23 @@ export function ConditionInput({
710743
<div
711744
className={cn(
712745
'flex items-center justify-between overflow-hidden bg-transparent px-[10px] py-[5px]',
713-
block.title === 'else'
714-
? 'rounded-[4px] border-0'
715-
: 'rounded-t-[4px] border-[var(--border-1)] border-b'
746+
isRouterMode
747+
? 'rounded-t-[4px] border-[var(--border-1)] border-b'
748+
: block.title === 'else'
749+
? 'rounded-[4px] border-0'
750+
: 'rounded-t-[4px] border-[var(--border-1)] border-b'
716751
)}
717752
>
718753
<span className='font-medium text-[14px] text-[var(--text-tertiary)]'>
719-
{block.title}
754+
{isRouterMode ? `Route ${index + 1}` : block.title}
720755
</span>
721756
<div className='flex items-center gap-[8px]'>
722757
<Tooltip.Root>
723758
<Tooltip.Trigger asChild>
724759
<Button
725760
variant='ghost'
726761
onClick={() => addBlock(block.id)}
727-
disabled={isPreview || disabled || block.title === 'else'}
762+
disabled={isPreview || disabled || (!isRouterMode && block.title === 'else')}
728763
className='h-auto p-0'
729764
>
730765
<Plus className='h-[14px] w-[14px]' />
@@ -739,7 +774,12 @@ export function ConditionInput({
739774
<Button
740775
variant='ghost'
741776
onClick={() => moveBlock(block.id, 'up')}
742-
disabled={isPreview || index === 0 || disabled || block.title === 'else'}
777+
disabled={
778+
isPreview ||
779+
index === 0 ||
780+
disabled ||
781+
(!isRouterMode && block.title === 'else')
782+
}
743783
className='h-auto p-0'
744784
>
745785
<ChevronUp className='h-[14px] w-[14px]' />
@@ -758,8 +798,8 @@ export function ConditionInput({
758798
isPreview ||
759799
disabled ||
760800
index === conditionalBlocks.length - 1 ||
761-
conditionalBlocks[index + 1]?.title === 'else' ||
762-
block.title === 'else'
801+
(!isRouterMode && conditionalBlocks[index + 1]?.title === 'else') ||
802+
(!isRouterMode && block.title === 'else')
763803
}
764804
className='h-auto p-0'
765805
>
@@ -775,18 +815,122 @@ export function ConditionInput({
775815
<Button
776816
variant='ghost'
777817
onClick={() => removeBlock(block.id)}
778-
disabled={isPreview || conditionalBlocks.length === 1 || disabled}
818+
disabled={isPreview || disabled || conditionalBlocks.length === 1}
779819
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
780820
>
781821
<Trash className='h-[14px] w-[14px]' />
782822
<span className='sr-only'>Delete Block</span>
783823
</Button>
784824
</Tooltip.Trigger>
785-
<Tooltip.Content>Delete Condition</Tooltip.Content>
825+
<Tooltip.Content>
826+
{isRouterMode ? 'Delete Route' : 'Delete Condition'}
827+
</Tooltip.Content>
786828
</Tooltip.Root>
787829
</div>
788830
</div>
789-
{block.title !== 'else' &&
831+
{/* Router mode: show description textarea with tag/env var support */}
832+
{isRouterMode && (
833+
<div
834+
className='relative'
835+
onDragOver={(e) => e.preventDefault()}
836+
onDrop={(e) => handleDrop(block.id, e)}
837+
>
838+
<Textarea
839+
data-router-block-id={block.id}
840+
value={block.value}
841+
onChange={(e) => {
842+
if (!isPreview && !disabled) {
843+
const newValue = e.target.value
844+
const pos = e.target.selectionStart ?? 0
845+
846+
const tagTrigger = checkTagTrigger(newValue, pos)
847+
const envVarTrigger = checkEnvVarTrigger(newValue, pos)
848+
849+
shouldPersistRef.current = true
850+
setConditionalBlocks((blocks) =>
851+
blocks.map((b) =>
852+
b.id === block.id
853+
? {
854+
...b,
855+
value: newValue,
856+
showTags: tagTrigger.show,
857+
showEnvVars: envVarTrigger.show,
858+
searchTerm: envVarTrigger.show ? envVarTrigger.searchTerm : '',
859+
cursorPosition: pos,
860+
}
861+
: b
862+
)
863+
)
864+
}
865+
}}
866+
onBlur={() => {
867+
setTimeout(() => {
868+
setConditionalBlocks((blocks) =>
869+
blocks.map((b) =>
870+
b.id === block.id ? { ...b, showTags: false, showEnvVars: false } : b
871+
)
872+
)
873+
}, 150)
874+
}}
875+
placeholder='Describe when this route should be taken...'
876+
disabled={disabled || isPreview}
877+
className='min-h-[60px] resize-none rounded-none border-0 px-3 py-2 text-sm placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
878+
rows={2}
879+
/>
880+
881+
{block.showEnvVars && (
882+
<EnvVarDropdown
883+
visible={block.showEnvVars}
884+
onSelect={(newValue) => handleEnvVarSelectImmediate(block.id, newValue)}
885+
searchTerm={block.searchTerm}
886+
inputValue={block.value}
887+
cursorPosition={block.cursorPosition}
888+
workspaceId={workspaceId}
889+
onClose={() => {
890+
setConditionalBlocks((blocks) =>
891+
blocks.map((b) =>
892+
b.id === block.id
893+
? {
894+
...b,
895+
showEnvVars: false,
896+
searchTerm: '',
897+
}
898+
: b
899+
)
900+
)
901+
}}
902+
/>
903+
)}
904+
905+
{block.showTags && (
906+
<TagDropdown
907+
visible={block.showTags}
908+
onSelect={(newValue) => handleTagSelectImmediate(block.id, newValue)}
909+
blockId={blockId}
910+
activeSourceBlockId={block.activeSourceBlockId}
911+
inputValue={block.value}
912+
cursorPosition={block.cursorPosition}
913+
onClose={() => {
914+
setConditionalBlocks((blocks) =>
915+
blocks.map((b) =>
916+
b.id === block.id
917+
? {
918+
...b,
919+
showTags: false,
920+
activeSourceBlockId: null,
921+
}
922+
: b
923+
)
924+
)
925+
}}
926+
/>
927+
)}
928+
</div>
929+
)}
930+
931+
{/* Condition mode: show code editor */}
932+
{!isRouterMode &&
933+
block.title !== 'else' &&
790934
(() => {
791935
const blockLineCount = block.value.split('\n').length
792936
const blockGutterWidth = calculateGutterWidth(blockLineCount)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,18 @@ function SubBlockComponent({
605605
/>
606606
)
607607

608+
case 'router-input':
609+
return (
610+
<ConditionInput
611+
blockId={blockId}
612+
subBlockId={config.id}
613+
isPreview={isPreview}
614+
previewValue={previewValue as any}
615+
disabled={isDisabled}
616+
mode='router'
617+
/>
618+
)
619+
608620
case 'eval-input':
609621
return (
610622
<EvalInput

0 commit comments

Comments
 (0)