Skip to content

Commit ffd56d1

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(ui): reuse shared cards for workflow inputs and variables
1 parent 0c53fa2 commit ffd56d1

5 files changed

Lines changed: 513 additions & 558 deletions

File tree

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

Lines changed: 135 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@ import {
33
Badge,
44
Button,
55
Code,
6+
CollapsibleCard,
67
Combobox,
78
type ComboboxOption,
89
calculateGutterWidth,
910
cn,
10-
Expandable,
11-
ExpandableContent,
1211
getCodeEditorProps,
13-
handleKeyboardActivation,
1412
highlight,
1513
Input,
1614
Label,
1715
languages,
16+
OverflowText,
1817
Tooltip,
1918
} from '@sim/emcn'
2019
import { ArrowLeftRight, Plus, Trash } from '@sim/emcn/icons'
@@ -388,52 +387,6 @@ export function FieldFormat({
388387
)
389388
}
390389

391-
/**
392-
* Renders the field header with name, type badge, and action buttons
393-
*/
394-
const renderFieldHeader = (field: Field, index: number) => (
395-
<div
396-
role='group'
397-
aria-label={`${title} ${index + 1}`}
398-
className='flex cursor-pointer items-center justify-between rounded-t-[3px] bg-[var(--surface-4)] px-2.5 py-[5px]'
399-
onClick={() => toggleCollapse(field.id)}
400-
onKeyDown={(event) => {
401-
if (event.target !== event.currentTarget) return
402-
handleKeyboardActivation(event, () => toggleCollapse(field.id))
403-
}}
404-
>
405-
<div className='flex min-w-0 flex-1 items-center gap-2'>
406-
<span className='block truncate text-[var(--text-tertiary)] text-sm'>
407-
{field.name || `${title} ${index + 1}`}
408-
</span>
409-
{field.name && showType && (
410-
<Badge variant='type' size='sm'>
411-
{field.type}
412-
</Badge>
413-
)}
414-
</div>
415-
<div
416-
role='presentation'
417-
className='flex items-center gap-2 pl-2'
418-
onClick={(e) => e.stopPropagation()}
419-
>
420-
<Button variant='ghost' onClick={addField} disabled={isReadOnly} className='h-auto p-0'>
421-
<Plus className='size-[14px]' />
422-
<span className='sr-only'>Add {title}</span>
423-
</Button>
424-
<Button
425-
variant='ghost-destructive'
426-
onClick={() => removeField(field.id)}
427-
disabled={isReadOnly}
428-
className='h-auto p-0 hover-hover:opacity-90'
429-
>
430-
<Trash className='size-[14px]' />
431-
<span className='sr-only'>Delete Field</span>
432-
</Button>
433-
</div>
434-
</div>
435-
)
436-
437390
/**
438391
* Renders the value input field based on the field type
439392
*/
@@ -670,113 +623,145 @@ export function FieldFormat({
670623
return (
671624
<div className='space-y-2'>
672625
{fields.map((field, index) => (
673-
<div
626+
<CollapsibleCard
674627
key={field.id}
675628
data-field-id={field.id}
676-
className='overflow-hidden rounded-sm border border-[var(--border-1)]'
629+
role='group'
630+
aria-label={`${title} ${index + 1}`}
631+
className='overflow-hidden'
632+
title={
633+
<span className='flex min-w-0 items-center gap-2'>
634+
<OverflowText
635+
label={field.name || `${title} ${index + 1}`}
636+
focusTarget='nearest-interactive'
637+
/>
638+
{field.name && showType && (
639+
<Badge variant='type' size='sm'>
640+
{field.type}
641+
</Badge>
642+
)}
643+
</span>
644+
}
645+
collapsed={!!field.collapsed}
646+
onToggleCollapse={() => toggleCollapse(field.id)}
647+
animated
648+
actions={
649+
<>
650+
<Button
651+
variant='ghost'
652+
onClick={addField}
653+
disabled={isReadOnly}
654+
className='h-auto p-0'
655+
>
656+
<Plus className='size-[14px]' />
657+
<span className='sr-only'>Add {title}</span>
658+
</Button>
659+
<Button
660+
variant='ghost-destructive'
661+
onClick={() => removeField(field.id)}
662+
disabled={isReadOnly}
663+
className='h-auto p-0 hover-hover:opacity-90'
664+
>
665+
<Trash className='size-[14px]' />
666+
<span className='sr-only'>Delete Field</span>
667+
</Button>
668+
</>
669+
}
677670
>
678-
{renderFieldHeader(field, index)}
679-
680-
<Expandable expanded={!field.collapsed}>
681-
<ExpandableContent>
682-
<div className='flex flex-col gap-2 rounded-b-[4px] border-[var(--border-1)] border-t bg-[var(--surface-2)] px-2.5 pt-1.5 pb-2.5'>
683-
<div className='flex flex-col gap-1.5'>
684-
{renderFieldLabel('Name')}
685-
<div className='relative'>{renderNameInput(field)}</div>
686-
</div>
671+
<div className='flex flex-col gap-1.5'>
672+
{renderFieldLabel('Name')}
673+
<div className='relative'>{renderNameInput(field)}</div>
674+
</div>
675+
676+
{showType && (
677+
<div className='flex flex-col gap-1.5'>
678+
{renderFieldLabel('Type')}
679+
<Combobox
680+
options={TYPE_OPTIONS}
681+
value={field.type}
682+
onChange={(value) => updateField(field.id, 'type', value)}
683+
disabled={isReadOnly}
684+
/>
685+
</div>
686+
)}
687687

688-
{showType && (
689-
<div className='flex flex-col gap-1.5'>
690-
{renderFieldLabel('Type')}
691-
<Combobox
692-
options={TYPE_OPTIONS}
693-
value={field.type}
694-
onChange={(value) => updateField(field.id, 'type', value)}
695-
disabled={isReadOnly}
696-
/>
697-
</div>
698-
)}
699-
700-
{showDescription && (
701-
<div className='flex flex-col gap-1.5'>
702-
{renderFieldLabel('Description')}
703-
<div className='relative'>
704-
<Input
705-
ref={(el) => {
706-
if (el) descriptionInputRefs.current[field.id] = el
707-
}}
708-
value={field.description ?? ''}
709-
onChange={(e) => updateField(field.id, 'description', e.target.value)}
710-
onScroll={(e) =>
711-
syncDescriptionOverlayScroll(field.id, e.currentTarget.scrollLeft)
712-
}
713-
onPaste={() =>
714-
setTimeout(() => {
715-
const input = descriptionInputRefs.current[field.id]
716-
input && syncDescriptionOverlayScroll(field.id, input.scrollLeft)
717-
}, 0)
718-
}
719-
placeholder={descriptionPlaceholder}
720-
disabled={isReadOnly}
721-
autoComplete='off'
722-
className='allow-scroll w-full overflow-x-auto overflow-y-hidden text-transparent caret-foreground [letter-spacing:inherit] placeholder:text-muted-foreground/50'
723-
/>
724-
<div
725-
ref={(el) => {
726-
if (el) descriptionOverlayRefs.current[field.id] = el
727-
}}
728-
style={{ scrollbarWidth: 'none' }}
729-
className={cn(
730-
'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 font-sans text-sm',
731-
isReadOnly && 'opacity-50'
732-
)}
733-
>
734-
<span className='w-full whitespace-pre' style={{ minWidth: 'fit-content' }}>
735-
{formatDisplayText(
736-
field.description ?? '',
737-
accessiblePrefixes
738-
? {
739-
accessiblePrefixes,
740-
workflowSearchHighlight: getActiveWorkflowSearchHighlight({
741-
activeSearchTarget,
742-
blockId,
743-
subBlockId,
744-
valuePath: [index, 'description'],
745-
}),
746-
}
747-
: {
748-
highlightAll: true,
749-
workflowSearchHighlight: getActiveWorkflowSearchHighlight({
750-
activeSearchTarget,
751-
blockId,
752-
subBlockId,
753-
valuePath: [index, 'description'],
754-
}),
755-
}
756-
)}
757-
</span>
758-
</div>
759-
</div>
760-
</div>
761-
)}
762-
763-
{showValue && (
764-
<div className='flex flex-col gap-1.5'>
765-
{isFileFieldType(field.type) ? (
766-
<div className='flex items-center justify-between'>
767-
{renderFieldLabel('Value')}
768-
{renderFileModeToggle(field)}
769-
</div>
770-
) : (
771-
renderFieldLabel('Value')
688+
{showDescription && (
689+
<div className='flex flex-col gap-1.5'>
690+
{renderFieldLabel('Description')}
691+
<div className='relative'>
692+
<Input
693+
ref={(el) => {
694+
if (el) descriptionInputRefs.current[field.id] = el
695+
}}
696+
value={field.description ?? ''}
697+
onChange={(e) => updateField(field.id, 'description', e.target.value)}
698+
onScroll={(e) =>
699+
syncDescriptionOverlayScroll(field.id, e.currentTarget.scrollLeft)
700+
}
701+
onPaste={() =>
702+
setTimeout(() => {
703+
const input = descriptionInputRefs.current[field.id]
704+
input && syncDescriptionOverlayScroll(field.id, input.scrollLeft)
705+
}, 0)
706+
}
707+
placeholder={descriptionPlaceholder}
708+
disabled={isReadOnly}
709+
autoComplete='off'
710+
className='allow-scroll w-full overflow-x-auto overflow-y-hidden text-transparent caret-foreground [letter-spacing:inherit] placeholder:text-muted-foreground/50'
711+
/>
712+
<div
713+
ref={(el) => {
714+
if (el) descriptionOverlayRefs.current[field.id] = el
715+
}}
716+
style={{ scrollbarWidth: 'none' }}
717+
className={cn(
718+
'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 font-sans text-sm',
719+
isReadOnly && 'opacity-50'
720+
)}
721+
>
722+
<span className='w-full whitespace-pre' style={{ minWidth: 'fit-content' }}>
723+
{formatDisplayText(
724+
field.description ?? '',
725+
accessiblePrefixes
726+
? {
727+
accessiblePrefixes,
728+
workflowSearchHighlight: getActiveWorkflowSearchHighlight({
729+
activeSearchTarget,
730+
blockId,
731+
subBlockId,
732+
valuePath: [index, 'description'],
733+
}),
734+
}
735+
: {
736+
highlightAll: true,
737+
workflowSearchHighlight: getActiveWorkflowSearchHighlight({
738+
activeSearchTarget,
739+
blockId,
740+
subBlockId,
741+
valuePath: [index, 'description'],
742+
}),
743+
}
772744
)}
773-
<div className='relative'>{renderValueInput(field)}</div>
774-
</div>
775-
)}
745+
</span>
746+
</div>
776747
</div>
777-
</ExpandableContent>
778-
</Expandable>
779-
</div>
748+
</div>
749+
)}
750+
751+
{showValue && (
752+
<div className='flex flex-col gap-1.5'>
753+
{isFileFieldType(field.type) ? (
754+
<div className='flex items-center justify-between'>
755+
{renderFieldLabel('Value')}
756+
{renderFileModeToggle(field)}
757+
</div>
758+
) : (
759+
renderFieldLabel('Value')
760+
)}
761+
<div className='relative'>{renderValueInput(field)}</div>
762+
</div>
763+
)}
764+
</CollapsibleCard>
780765
))}
781766
</div>
782767
)

0 commit comments

Comments
 (0)