Skip to content

Commit db20421

Browse files
committed
fix(input-format): file-field mode toggle uses canonical arrow icon on the label row
The file field's mode switch was a right-aligned 'Enter JSON manually' text link on its own row, causing awkward spacing above the control. Replace it with the canonical sub-block mode toggle: a compact left-right arrows (ArrowLeftRight) icon button with a tooltip, placed on the 'Value' label row (label left, toggle right), matching the Files subblock header. Removes the extra row and aligns spacing.
1 parent 604d03e commit db20421

1 file changed

Lines changed: 98 additions & 64 deletions

File tree

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

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

Lines changed: 98 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import {
1515
Input,
1616
Label,
1717
languages,
18+
Tooltip,
1819
} from '@sim/emcn'
1920
import { Trash } from '@sim/emcn/icons'
20-
import { Plus } from 'lucide-react'
21+
import { ArrowLeftRight, Plus } from 'lucide-react'
2122
import Editor from 'react-simple-code-editor'
2223
import {
2324
createDefaultInputFormatField,
@@ -141,6 +142,59 @@ export function FieldFormat({
141142

142143
const renderFieldLabel = (label: string) => <Label>{label}</Label>
143144

145+
/**
146+
* Resolves the current editor mode for a file field. The uploader is only
147+
* offered when it can represent the stored value losslessly (empty or all
148+
* run-ready); mixed/legacy values force JSON mode so the uploader can't drop
149+
* entries it cannot show on save.
150+
*/
151+
const getFileFieldMode = (field: Field): { mode: 'upload' | 'json'; canUseUploader: boolean } => {
152+
const canUseUploader = defaultFileFieldMode(field.value) === 'upload'
153+
return {
154+
mode: canUseUploader ? (fileFieldModes[field.id] ?? 'upload') : 'json',
155+
canUseUploader,
156+
}
157+
}
158+
159+
/**
160+
* Renders the ⇄ toggle that switches a file field between the uploader and the
161+
* raw JSON editor. Matches the canonical sub-block mode toggle. Hidden when the
162+
* value can't be safely represented by the uploader.
163+
*/
164+
const renderFileModeToggle = (field: Field) => {
165+
const { mode, canUseUploader } = getFileFieldMode(field)
166+
if (!canUseUploader) return null
167+
const label = mode === 'upload' ? 'Switch to JSON' : 'Switch to file uploader'
168+
return (
169+
<Tooltip.Root>
170+
<Tooltip.Trigger asChild>
171+
<button
172+
type='button'
173+
className='flex size-[12px] flex-shrink-0 items-center justify-center bg-transparent p-0 disabled:cursor-not-allowed disabled:opacity-50'
174+
onClick={() =>
175+
setFileFieldModes((prev) => ({
176+
...prev,
177+
[field.id]: mode === 'upload' ? 'json' : 'upload',
178+
}))
179+
}
180+
disabled={isReadOnly}
181+
aria-label={label}
182+
>
183+
<ArrowLeftRight
184+
className={cn(
185+
'!h-[12px] !w-[12px]',
186+
mode === 'json' ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)]'
187+
)}
188+
/>
189+
</button>
190+
</Tooltip.Trigger>
191+
<Tooltip.Content side='top'>
192+
<p>{label}</p>
193+
</Tooltip.Content>
194+
</Tooltip.Root>
195+
)
196+
}
197+
144198
/**
145199
* Adds a new field to the list
146200
*/
@@ -493,52 +547,28 @@ export function FieldFormat({
493547
}
494548

495549
if (isFileFieldType(field.type)) {
496-
// The uploader is only offered when it can represent the stored value
497-
// losslessly (empty or all run-ready). For mixed/legacy values it would
498-
// drop the entries it can't show on save, so we force JSON mode and hide
499-
// the toggle until the value is cleared or made fully run-ready.
500-
const canUseUploader = defaultFileFieldMode(field.value) === 'upload'
501-
const mode = canUseUploader ? (fileFieldModes[field.id] ?? 'upload') : 'json'
502-
503-
const modeToggle = canUseUploader ? (
504-
<div className='flex justify-end'>
505-
<Button
506-
type='button'
507-
variant='ghost'
508-
onClick={() =>
509-
setFileFieldModes((prev) => ({
510-
...prev,
511-
[field.id]: mode === 'upload' ? 'json' : 'upload',
512-
}))
513-
}
514-
disabled={isReadOnly}
515-
className='h-auto p-0 text-[var(--text-muted)] text-xs hover-hover:text-[var(--text-body)]'
516-
>
517-
{mode === 'upload' ? 'Enter JSON manually' : 'Use file uploader'}
518-
</Button>
519-
</div>
520-
) : null
550+
// The mode toggle lives on the "Value" label row (see the field header);
551+
// this only renders the active control. Mode derivation is shared via
552+
// getFileFieldMode so the two stay in sync.
553+
const { mode } = getFileFieldMode(field)
521554

522555
if (mode === 'upload') {
523556
const currentFiles = parseInputFormatFiles(field.value)
524557
return (
525-
<div className='flex flex-col gap-1.5'>
526-
{modeToggle}
527-
<FileUpload
528-
blockId={blockId}
529-
subBlockId={subBlockId}
530-
multiple
531-
disabled={isReadOnly}
532-
value={filesToControlValue(currentFiles)}
533-
onValueChange={(next) =>
534-
updateField(
535-
field.id,
536-
'value',
537-
serializeInputFormatFiles(controlValueToFiles(next, currentFiles))
538-
)
539-
}
540-
/>
541-
</div>
558+
<FileUpload
559+
blockId={blockId}
560+
subBlockId={subBlockId}
561+
multiple
562+
disabled={isReadOnly}
563+
value={filesToControlValue(currentFiles)}
564+
onValueChange={(next) =>
565+
updateField(
566+
field.id,
567+
'value',
568+
serializeInputFormatFiles(controlValueToFiles(next, currentFiles))
569+
)
570+
}
571+
/>
542572
)
543573
}
544574

@@ -556,26 +586,23 @@ export function FieldFormat({
556586
))
557587

558588
return (
559-
<div className='flex flex-col gap-1.5'>
560-
{modeToggle}
561-
<Code.Container className='min-h-[120px]'>
562-
<Code.Gutter width={gutterWidth}>{renderLineNumbers()}</Code.Gutter>
563-
<Code.Content paddingLeft={`${gutterWidth}px`}>
564-
<Code.Placeholder gutterWidth={gutterWidth} show={fieldValue.length === 0}>
565-
{
566-
'[\n {\n "data": "<base64>",\n "type": "file",\n "name": "document.pdf",\n "mime": "application/pdf"\n }\n]'
567-
}
568-
</Code.Placeholder>
569-
<Editor
570-
value={fieldValue}
571-
onValueChange={getEditorValueChangeHandler(field.id)}
572-
highlight={jsonHighlight}
573-
disabled={isReadOnly}
574-
{...getCodeEditorProps({ disabled: isReadOnly })}
575-
/>
576-
</Code.Content>
577-
</Code.Container>
578-
</div>
589+
<Code.Container className='min-h-[120px]'>
590+
<Code.Gutter width={gutterWidth}>{renderLineNumbers()}</Code.Gutter>
591+
<Code.Content paddingLeft={`${gutterWidth}px`}>
592+
<Code.Placeholder gutterWidth={gutterWidth} show={fieldValue.length === 0}>
593+
{
594+
'[\n {\n "data": "<base64>",\n "type": "file",\n "name": "document.pdf",\n "mime": "application/pdf"\n }\n]'
595+
}
596+
</Code.Placeholder>
597+
<Editor
598+
value={fieldValue}
599+
onValueChange={getEditorValueChangeHandler(field.id)}
600+
highlight={jsonHighlight}
601+
disabled={isReadOnly}
602+
{...getCodeEditorProps({ disabled: isReadOnly })}
603+
/>
604+
</Code.Content>
605+
</Code.Container>
579606
)
580607
}
581608

@@ -709,7 +736,14 @@ export function FieldFormat({
709736

710737
{showValue && (
711738
<div className='flex flex-col gap-1.5'>
712-
{renderFieldLabel('Value')}
739+
{isFileFieldType(field.type) ? (
740+
<div className='flex items-center justify-between'>
741+
{renderFieldLabel('Value')}
742+
{renderFileModeToggle(field)}
743+
</div>
744+
) : (
745+
renderFieldLabel('Value')
746+
)}
713747
<div className='relative'>{renderValueInput(field)}</div>
714748
</div>
715749
)}

0 commit comments

Comments
 (0)