@@ -15,9 +15,10 @@ import {
1515 Input ,
1616 Label ,
1717 languages ,
18+ Tooltip ,
1819} from '@sim/emcn'
1920import { Trash } from '@sim/emcn/icons'
20- import { Plus } from 'lucide-react'
21+ import { ArrowLeftRight , Plus } from 'lucide-react'
2122import Editor from 'react-simple-code-editor'
2223import {
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