Skip to content

Commit a067bdf

Browse files
refactor(emcn): extract CollapsibleCard and reuse for input mapping
Pull the collapsible field-card markup (surface-4 header + surface-2 body, click/keyboard toggle, truncated title + optional badge) into a shared `CollapsibleCard` emcn component, and use it in the workflow-builder input mapping rows and the table sidebar's input-mapping panel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e1268f commit a067bdf

4 files changed

Lines changed: 170 additions & 152 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/input-mapping-section.tsx

Lines changed: 32 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { Badge, Combobox, Label } from '@/components/emcn'
5-
import { cn } from '@/lib/core/utils/cn'
6-
import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
4+
import { Badge, CollapsibleCard, Combobox, Label } from '@/components/emcn'
75
import type { ColumnDefinition } from '@/lib/table'
86
import type { InputFormatField } from '@/lib/workflows/types'
97

@@ -49,59 +47,37 @@ export function InputMappingSection({
4947
</p>
5048
) : (
5149
<div className='flex flex-col gap-2'>
52-
{namedFields.map((field) => {
53-
const isCollapsed = collapsed[field.name] ?? false
54-
return (
55-
<div
56-
key={field.name}
57-
className={cn(
58-
'rounded-sm border border-[var(--border-1)]',
59-
isCollapsed ? 'overflow-hidden' : 'overflow-visible'
60-
)}
61-
>
62-
<div
63-
role='button'
64-
tabIndex={0}
65-
className='flex cursor-pointer items-center justify-between rounded-t-[4px] bg-[var(--surface-4)] px-2.5 py-[5px]'
66-
onClick={() => toggle(field.name)}
67-
onKeyDown={(event) => handleKeyboardActivation(event, () => toggle(field.name))}
68-
>
69-
<div className='flex min-w-0 flex-1 items-center gap-2'>
70-
<span className='block truncate font-medium text-[var(--text-tertiary)] text-sm'>
71-
{field.name}
72-
</span>
73-
{field.type && (
74-
<Badge variant='type' size='sm'>
75-
{field.type}
76-
</Badge>
77-
)}
78-
</div>
79-
</div>
80-
81-
{!isCollapsed && (
82-
<div className='flex flex-col gap-1.5 rounded-b-[4px] border-[var(--border-1)] border-t bg-[var(--surface-2)] px-2.5 pt-1.5 pb-2.5'>
83-
<Label className='text-small'>Column</Label>
84-
<Combobox
85-
searchable
86-
searchPlaceholder='Search columns…'
87-
size='sm'
88-
className='h-[32px] w-full rounded-md'
89-
dropdownWidth='trigger'
90-
maxHeight={240}
91-
disabled={columns.length === 0}
92-
emptyMessage='No columns.'
93-
placeholder='Select a column'
94-
options={columns}
95-
value={value[field.name] ?? ''}
96-
onChange={(columnName: string) =>
97-
onChange({ ...value, [field.name]: columnName })
98-
}
99-
/>
100-
</div>
101-
)}
102-
</div>
103-
)
104-
})}
50+
{namedFields.map((field) => (
51+
<CollapsibleCard
52+
key={field.name}
53+
title={field.name}
54+
badge={
55+
field.type ? (
56+
<Badge variant='type' size='sm'>
57+
{field.type}
58+
</Badge>
59+
) : undefined
60+
}
61+
collapsed={collapsed[field.name] ?? false}
62+
onToggleCollapse={() => toggle(field.name)}
63+
>
64+
<Label className='text-small'>Column</Label>
65+
<Combobox
66+
searchable
67+
searchPlaceholder='Search columns…'
68+
size='sm'
69+
className='h-[32px] w-full rounded-md'
70+
dropdownWidth='trigger'
71+
maxHeight={240}
72+
disabled={columns.length === 0}
73+
emptyMessage='No columns.'
74+
placeholder='Select a column'
75+
options={columns}
76+
value={value[field.name] ?? ''}
77+
onChange={(columnName: string) => onChange({ ...value, [field.name]: columnName })}
78+
/>
79+
</CollapsibleCard>
80+
))}
10581
</div>
10682
)}
10783
</div>

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

Lines changed: 76 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { useEffect, useMemo, useRef, useState } from 'react'
2-
import { Badge, Input } from '@/components/emcn'
2+
import { Badge, CollapsibleCard, Input } from '@/components/emcn'
33
import { Label } from '@/components/ui/label'
44
import { cn } from '@/lib/core/utils/cn'
5-
import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
65
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
76
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
87
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
@@ -242,104 +241,85 @@ function InputMappingField({
242241
}
243242

244243
return (
245-
<div
246-
className={cn(
247-
'rounded-sm border border-[var(--border-1)]',
248-
collapsed ? 'overflow-hidden' : 'overflow-visible'
249-
)}
244+
<CollapsibleCard
245+
title={fieldName}
246+
badge={
247+
fieldType ? (
248+
<Badge variant='type' size='sm'>
249+
{fieldType}
250+
</Badge>
251+
) : undefined
252+
}
253+
collapsed={collapsed}
254+
onToggleCollapse={onToggleCollapse}
250255
>
251-
<div
252-
role='button'
253-
tabIndex={0}
254-
className='flex cursor-pointer items-center justify-between rounded-t-[4px] bg-[var(--surface-4)] px-2.5 py-[5px]'
255-
onClick={onToggleCollapse}
256-
onKeyDown={(event) => handleKeyboardActivation(event, onToggleCollapse)}
257-
>
258-
<div className='flex min-w-0 flex-1 items-center gap-2'>
259-
<span className='block truncate font-medium text-[var(--text-tertiary)] text-sm'>
260-
{fieldName}
261-
</span>
262-
{fieldType && (
263-
<Badge variant='type' size='sm'>
264-
{fieldType}
265-
</Badge>
266-
)}
267-
</div>
268-
</div>
269-
270-
{!collapsed && (
271-
<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'>
272-
<div className='flex flex-col gap-1.5'>
273-
<Label className='text-small'>Value</Label>
274-
<div className='relative'>
275-
<Input
276-
ref={(el) => {
277-
if (el) inputRefs.current.set(fieldId, el)
278-
}}
279-
name='value'
280-
value={value}
281-
onChange={handlers.onChange}
282-
onKeyDown={handlers.onKeyDown}
283-
onDrop={handlers.onDrop}
284-
onDragOver={handlers.onDragOver}
285-
onFocus={handlers.onFocus}
286-
onScroll={(e) => handleScroll(e)}
287-
onPaste={() =>
288-
setTimeout(() => {
289-
const input = inputRefs.current.get(fieldId)
290-
input && handleScroll({ currentTarget: input } as any)
291-
}, 0)
292-
}
293-
placeholder='Enter value or reference'
294-
disabled={disabled}
295-
autoComplete='off'
296-
className={cn(
297-
'allow-scroll w-full overflow-auto text-transparent caret-foreground'
298-
)}
299-
style={{ overflowX: 'auto' }}
300-
/>
301-
<div
302-
ref={(el) => {
303-
if (el) overlayRefs.current.set(fieldId, el)
304-
}}
305-
className={cn(
306-
'absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 font-medium font-sans text-sm',
307-
!disabled && 'pointer-events-none'
308-
)}
309-
style={{ overflowX: 'auto' }}
310-
>
311-
<div
312-
className='w-full whitespace-pre'
313-
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
314-
>
315-
{formatDisplayText(
316-
value,
317-
accessiblePrefixes
318-
? { accessiblePrefixes, workflowSearchHighlight }
319-
: { highlightAll: true, workflowSearchHighlight }
320-
)}
321-
</div>
322-
</div>
323-
{fieldState.showTags && (
324-
<TagDropdown
325-
visible={fieldState.showTags}
326-
onSelect={tagSelectHandler}
327-
blockId={blockId}
328-
activeSourceBlockId={fieldState.activeSourceBlockId}
329-
inputValue={value}
330-
cursorPosition={fieldState.cursorPosition}
331-
onClose={() => inputController.fieldHelpers.hideFieldDropdowns(fieldId)}
332-
inputRef={
333-
{
334-
current: inputRefs.current.get(fieldId) || null,
335-
} as React.RefObject<HTMLInputElement>
336-
}
337-
/>
256+
<div className='flex flex-col gap-1.5'>
257+
<Label className='text-small'>Value</Label>
258+
<div className='relative'>
259+
<Input
260+
ref={(el) => {
261+
if (el) inputRefs.current.set(fieldId, el)
262+
}}
263+
name='value'
264+
value={value}
265+
onChange={handlers.onChange}
266+
onKeyDown={handlers.onKeyDown}
267+
onDrop={handlers.onDrop}
268+
onDragOver={handlers.onDragOver}
269+
onFocus={handlers.onFocus}
270+
onScroll={(e) => handleScroll(e)}
271+
onPaste={() =>
272+
setTimeout(() => {
273+
const input = inputRefs.current.get(fieldId)
274+
input && handleScroll({ currentTarget: input } as any)
275+
}, 0)
276+
}
277+
placeholder='Enter value or reference'
278+
disabled={disabled}
279+
autoComplete='off'
280+
className={cn('allow-scroll w-full overflow-auto text-transparent caret-foreground')}
281+
style={{ overflowX: 'auto' }}
282+
/>
283+
<div
284+
ref={(el) => {
285+
if (el) overlayRefs.current.set(fieldId, el)
286+
}}
287+
className={cn(
288+
'absolute inset-0 flex items-center overflow-x-auto bg-transparent px-2 py-1.5 font-medium font-sans text-sm',
289+
!disabled && 'pointer-events-none'
290+
)}
291+
style={{ overflowX: 'auto' }}
292+
>
293+
<div
294+
className='w-full whitespace-pre'
295+
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
296+
>
297+
{formatDisplayText(
298+
value,
299+
accessiblePrefixes
300+
? { accessiblePrefixes, workflowSearchHighlight }
301+
: { highlightAll: true, workflowSearchHighlight }
338302
)}
339303
</div>
340304
</div>
305+
{fieldState.showTags && (
306+
<TagDropdown
307+
visible={fieldState.showTags}
308+
onSelect={tagSelectHandler}
309+
blockId={blockId}
310+
activeSourceBlockId={fieldState.activeSourceBlockId}
311+
inputValue={value}
312+
cursorPosition={fieldState.cursorPosition}
313+
onClose={() => inputController.fieldHelpers.hideFieldDropdowns(fieldId)}
314+
inputRef={
315+
{
316+
current: inputRefs.current.get(fieldId) || null,
317+
} as React.RefObject<HTMLInputElement>
318+
}
319+
/>
320+
)}
341321
</div>
342-
)}
343-
</div>
322+
</div>
323+
</CollapsibleCard>
344324
)
345325
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use client'
2+
3+
import type * as React from 'react'
4+
import { cn } from '@/lib/core/utils/cn'
5+
import { handleKeyboardActivation } from '@/lib/core/utils/keyboard'
6+
7+
export interface CollapsibleCardProps {
8+
/** Header label (rendered in the standard truncated field-title style). */
9+
title: React.ReactNode
10+
/** Optional trailing header content, e.g. a type `Badge`. */
11+
badge?: React.ReactNode
12+
collapsed: boolean
13+
onToggleCollapse: () => void
14+
/** Body content, shown when expanded. */
15+
children: React.ReactNode
16+
className?: string
17+
}
18+
19+
/**
20+
* A collapsible field card: a `--surface-4` header (click / keyboard to toggle)
21+
* with a truncated title + optional badge, over a `--surface-2` body. Shared by
22+
* the workflow input-mapping rows and the enrichment output-column config.
23+
*/
24+
export function CollapsibleCard({
25+
title,
26+
badge,
27+
collapsed,
28+
onToggleCollapse,
29+
children,
30+
className,
31+
}: CollapsibleCardProps) {
32+
return (
33+
<div
34+
className={cn(
35+
'rounded-sm border border-[var(--border-1)]',
36+
collapsed ? 'overflow-hidden' : 'overflow-visible',
37+
className
38+
)}
39+
>
40+
<div
41+
role='button'
42+
tabIndex={0}
43+
className='flex cursor-pointer items-center justify-between rounded-t-[4px] bg-[var(--surface-4)] px-2.5 py-[5px]'
44+
onClick={onToggleCollapse}
45+
onKeyDown={(event) => handleKeyboardActivation(event, onToggleCollapse)}
46+
>
47+
<div className='flex min-w-0 flex-1 items-center gap-2'>
48+
<span className='block truncate font-medium text-[var(--text-tertiary)] text-sm'>
49+
{title}
50+
</span>
51+
{badge}
52+
</div>
53+
</div>
54+
{!collapsed && (
55+
<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'>
56+
{children}
57+
</div>
58+
)}
59+
</div>
60+
)
61+
}

apps/sim/components/emcn/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export {
3030
languages,
3131
} from './code/code'
3232
export { CopyCodeButton } from './code/copy-code-button'
33+
export { CollapsibleCard, type CollapsibleCardProps } from './collapsible-card/collapsible-card'
3334
export {
3435
Combobox,
3536
type ComboboxOption,

0 commit comments

Comments
 (0)