Skip to content

Commit 1e1268f

Browse files
feat(tables): native enrichments sidebar + workflow input mapping
Add a Clay-style enrichments catalog to the table view and wire per-row input mapping into workflow-backed columns. - New "Enrichments" entry in the New-column dropdown opens a sliding panel listing curated enrichment templates; picking one swaps to the workflow config in-place (no cross-slide) with a back button. - Type the workflow sidebar as manual | enrichment; enrichment hides the launch + add-column-inputs affordances. - Add a "Workflow inputs" advanced panel mapping Start-block input fields to table columns (left-of-workflow columns only), with name-match auto-fill and collapsible input-mapping-style rows. - Persist type + inputMappings on the workflow group (types, contract, route, service, hook) — jsonb, no migration. - Consume inputMappings at run time: when present, feed Start-block fields from the mapped columns; otherwise fall back to name-match spread. - Clean up inputMappings on column rename/delete (stripGroupDeps + renameColumn). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2b8068c commit 1e1268f

16 files changed

Lines changed: 662 additions & 73 deletions

File tree

apps/sim/app/api/table/[tableId]/groups/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ export const PATCH = withRouteHandler(async (request: NextRequest, { params }: R
113113
...(validated.mappingUpdates !== undefined
114114
? { mappingUpdates: validated.mappingUpdates }
115115
: {}),
116+
...(validated.inputMappings !== undefined
117+
? { inputMappings: validated.inputMappings }
118+
: {}),
119+
...(validated.type !== undefined ? { type: validated.type } : {}),
116120
...(validated.autoRun !== undefined ? { autoRun: validated.autoRun } : {}),
117121
},
118122
requestId
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
'use client'
2+
3+
import type React from 'react'
4+
import { useState } from 'react'
5+
import {
6+
Briefcase,
7+
Building2,
8+
DollarSign,
9+
Globe,
10+
Link2,
11+
Mail,
12+
Sparkles,
13+
TrendingUp,
14+
X,
15+
} from 'lucide-react'
16+
import { Button, Input } from '@/components/emcn'
17+
import { Search } from '@/components/emcn/icons'
18+
import { cn } from '@/lib/core/utils/cn'
19+
import type { ColumnDefinition, WorkflowGroup } from '@/lib/table'
20+
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
21+
import { generateColumnName } from '../../utils'
22+
import { type WorkflowConfig, WorkflowSidebarBody } from '../workflow-sidebar'
23+
24+
/** A shared enrichment a user can drop onto a table as a workflow column. */
25+
export interface EnrichmentTemplate {
26+
id: string
27+
name: string
28+
description: string
29+
icon: React.ComponentType<{ className?: string }>
30+
}
31+
32+
/**
33+
* Curated catalog shown in the enrichments list. Stand-in data until a real
34+
* shared-workflow catalog exists — every card currently resolves to the first
35+
* workspace workflow as its template (see `Table`'s `onPickEnrichment`).
36+
*/
37+
const ENRICHMENT_TEMPLATES: EnrichmentTemplate[] = [
38+
{
39+
id: 'use-ai',
40+
name: 'Use AI',
41+
description: 'Run a custom AI prompt over each row.',
42+
icon: Sparkles,
43+
},
44+
{ id: 'work-email', name: 'Work Email', description: "Find a person's work email.", icon: Mail },
45+
{
46+
id: 'company-domain',
47+
name: 'Company Domain',
48+
description: 'Find a domain address from a company name.',
49+
icon: Link2,
50+
},
51+
{
52+
id: 'website-traffic',
53+
name: 'Website Traffic (Monthly)',
54+
description: 'Get the monthly website traffic for a domain.',
55+
icon: TrendingUp,
56+
},
57+
{
58+
id: 'company-funding',
59+
name: 'Company Latest Funding',
60+
description: "Look up a company's latest funding details.",
61+
icon: DollarSign,
62+
},
63+
{
64+
id: 'website-techstack',
65+
name: 'Website Techstack',
66+
description: 'See what technologies a website uses.',
67+
icon: Globe,
68+
},
69+
{
70+
id: 'company-revenue',
71+
name: 'Company Revenue',
72+
description: "Find a company's revenue.",
73+
icon: Building2,
74+
},
75+
{
76+
id: 'company-jobs',
77+
name: 'Company Job Openings',
78+
description: "Look up a company's current job openings.",
79+
icon: Briefcase,
80+
},
81+
]
82+
83+
interface EnrichmentsSidebarProps {
84+
open: boolean
85+
onClose: () => void
86+
/** Forwarded to the hosted workflow body — same props `WorkflowSidebar` takes. */
87+
allColumns: ColumnDefinition[]
88+
workflowGroups: WorkflowGroup[]
89+
workflows: WorkflowMetadata[] | undefined
90+
workspaceId: string
91+
tableId: string
92+
onColumnRename?: (oldName: string, newName: string) => void
93+
}
94+
95+
/**
96+
* Right-edge panel for the enrichments flow. Hosts both the catalog list and
97+
* (once a card is picked) the workflow-config body in the *same* sliding panel,
98+
* so picking an enrichment swaps content in place rather than cross-sliding a
99+
* second panel over the list.
100+
*/
101+
export function EnrichmentsSidebar({ open, ...rest }: EnrichmentsSidebarProps) {
102+
return (
103+
<aside
104+
role='dialog'
105+
aria-label='Enrichments'
106+
className={cn(
107+
'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] shadow-overlay transition-transform duration-200 ease-out',
108+
open ? 'translate-x-0' : 'translate-x-full'
109+
)}
110+
>
111+
{open && <EnrichmentsSidebarBody {...rest} />}
112+
</aside>
113+
)
114+
}
115+
116+
function EnrichmentsSidebarBody({
117+
onClose,
118+
allColumns,
119+
workflowGroups,
120+
workflows,
121+
workspaceId,
122+
tableId,
123+
onColumnRename,
124+
}: Omit<EnrichmentsSidebarProps, 'open'>) {
125+
const [selected, setSelected] = useState<EnrichmentTemplate | null>(null)
126+
const [query, setQuery] = useState('')
127+
128+
// A card is picked — show the workflow-config body in this same panel. The
129+
// `key` remounts the body when the selection (or its resolved workflow)
130+
// changes so its form state re-seeds.
131+
if (selected) {
132+
const workflowId = workflows?.[0]?.id
133+
const config: WorkflowConfig = {
134+
mode: 'create',
135+
kind: 'enrichment',
136+
proposedName: generateColumnName(allColumns),
137+
workflowId,
138+
enrichmentName: selected.name,
139+
}
140+
return (
141+
<WorkflowSidebarBody
142+
key={`${selected.id}:${workflowId ?? ''}`}
143+
config={config}
144+
onClose={onClose}
145+
allColumns={allColumns}
146+
workflowGroups={workflowGroups}
147+
workflows={workflows}
148+
workspaceId={workspaceId}
149+
tableId={tableId}
150+
onColumnRename={onColumnRename}
151+
onBack={() => setSelected(null)}
152+
/>
153+
)
154+
}
155+
156+
const normalized = query.trim().toLowerCase()
157+
const filtered = normalized
158+
? ENRICHMENT_TEMPLATES.filter(
159+
(t) =>
160+
t.name.toLowerCase().includes(normalized) ||
161+
t.description.toLowerCase().includes(normalized)
162+
)
163+
: ENRICHMENT_TEMPLATES
164+
165+
return (
166+
<div className='flex h-full flex-col'>
167+
<div className='flex items-center justify-between border-[var(--border)] border-b px-3 py-[8.5px]'>
168+
<h2 className='font-medium text-[var(--text-primary)] text-small'>Enrichments</h2>
169+
<Button
170+
variant='ghost'
171+
size='sm'
172+
onClick={onClose}
173+
className='!p-1 size-7 flex-none'
174+
aria-label='Close'
175+
>
176+
<X className='size-[14px]' />
177+
</Button>
178+
</div>
179+
180+
<div className='px-2 pt-3'>
181+
<div className='relative'>
182+
<Search className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-2 size-[14px] text-[var(--text-muted)]' />
183+
<Input
184+
value={query}
185+
onChange={(e) => setQuery(e.target.value)}
186+
placeholder='Search'
187+
spellCheck={false}
188+
autoComplete='off'
189+
className='pl-7'
190+
/>
191+
</div>
192+
</div>
193+
194+
<div className='flex-1 overflow-y-auto overflow-x-hidden px-2 py-3 [overflow-anchor:none]'>
195+
{filtered.length === 0 ? (
196+
<p className='px-1 pt-2 text-[var(--text-tertiary)] text-small'>No enrichments found.</p>
197+
) : (
198+
<ul className='flex flex-col'>
199+
{filtered.map((template) => {
200+
const Icon = template.icon
201+
return (
202+
<li key={template.id}>
203+
<Button
204+
variant='ghost'
205+
type='button'
206+
onClick={() => setSelected(template)}
207+
className='flex w-full items-start justify-start gap-2.5 rounded-md px-2 py-2 text-left hover-hover:bg-[var(--surface-3)]'
208+
>
209+
<Icon className='mt-0.5 size-[14px] flex-none text-[var(--text-icon)]' />
210+
<span className='flex min-w-0 flex-col gap-0.5'>
211+
<span className='truncate font-medium text-[var(--text-primary)] text-small'>
212+
{template.name}
213+
</span>
214+
<span className='truncate text-[var(--text-tertiary)] text-caption'>
215+
{template.description}
216+
</span>
217+
</span>
218+
</Button>
219+
</li>
220+
)
221+
})}
222+
</ul>
223+
)}
224+
</div>
225+
</div>
226+
)
227+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { EnrichmentsSidebar, type EnrichmentTemplate } from './enrichments-sidebar'

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './column-config-sidebar'
22
export * from './context-menu'
3+
export * from './enrichments-sidebar'
34
export * from './new-column-dropdown'
45
export * from './row-modal'
56
export * from './run-status-control'

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
'use client'
22

3+
import { Sparkles } from 'lucide-react'
34
import {
45
Button,
56
DropdownMenu,
67
DropdownMenuContent,
78
DropdownMenuItem,
9+
DropdownMenuSeparator,
810
DropdownMenuTrigger,
911
} from '@/components/emcn'
1012
import { Plus } from '@/components/emcn/icons'
@@ -28,18 +30,20 @@ interface NewColumnDropdownProps {
2830
disabled: boolean
2931
onPickType: (type: ColumnDefinition['type']) => void
3032
onPickWorkflow: () => void
33+
onPickEnrichment: () => void
3134
}
3235

3336
/**
3437
* "+ New column" dropdown — the single entry point for creating a column.
35-
* Lists every column type plus "Workflow"; picking a type opens the right
36-
* sidebar pre-seeded.
38+
* Lists every column type plus "Workflow" and "Enrichments"; picking a type
39+
* opens the right sidebar pre-seeded.
3740
*/
3841
export function NewColumnDropdown({
3942
trigger,
4043
disabled,
4144
onPickType,
4245
onPickWorkflow,
46+
onPickEnrichment,
4347
}: NewColumnDropdownProps) {
4448
const menu = (
4549
<DropdownMenu>
@@ -61,6 +65,15 @@ export function NewColumnDropdown({
6165
)}
6266
</DropdownMenuTrigger>
6367
<DropdownMenuContent align='start' side='bottom' sideOffset={4}>
68+
{isWorkflowColumnsEnabledClient && (
69+
<>
70+
<DropdownMenuItem onSelect={onPickEnrichment}>
71+
<Sparkles className='size-[14px] text-[var(--text-icon)]' />
72+
Enrichments
73+
</DropdownMenuItem>
74+
<DropdownMenuSeparator />
75+
</>
76+
)}
6477
{VISIBLE_COLUMN_TYPE_OPTIONS.map((option) => {
6578
const Icon = option.icon
6679
const onSelect =

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ interface TableGridProps {
139139
*/
140140
onOpenColumnConfig: (cfg: ColumnConfig) => void
141141
onOpenWorkflowConfig: (cfg: WorkflowConfig) => void
142+
/** Open the enrichments list (Clay-style catalog) slideout. */
143+
onOpenEnrichments: () => void
142144
onOpenExecutionDetails: (executionId: string) => void
143145
/** Open the row-edit modal for `row`. Wrapper renders the modal. */
144146
onOpenRowModal: (row: TableRowType) => void
@@ -243,6 +245,7 @@ export function TableGrid({
243245
sidebarReservedPx,
244246
onOpenColumnConfig,
245247
onOpenWorkflowConfig,
248+
onOpenEnrichments,
246249
onOpenExecutionDetails,
247250
onOpenRowModal,
248251
onRequestDeleteRows,
@@ -2560,7 +2563,7 @@ export function TableGrid({
25602563

25612564
/** Open the workflow-config sidebar to spawn a brand-new workflow group. */
25622565
function handleAddWorkflowColumn() {
2563-
onOpenWorkflowConfig({ mode: 'create', proposedName: generateColumnName() })
2566+
onOpenWorkflowConfig({ mode: 'create', kind: 'manual', proposedName: generateColumnName() })
25642567
}
25652568

25662569
const handleConfigureColumn = useCallback(
@@ -3229,6 +3232,7 @@ export function TableGrid({
32293232
disabled={addColumnMutation.isPending}
32303233
onPickType={handleAddColumnOfType}
32313234
onPickWorkflow={handleAddWorkflowColumn}
3235+
onPickEnrichment={onOpenEnrichments}
32323236
/>
32333237
)}
32343238
</tr>
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
export { type WorkflowConfig, WorkflowSidebar } from './workflow-sidebar'
1+
export {
2+
type WorkflowConfig,
3+
WorkflowSidebar,
4+
WorkflowSidebarBody,
5+
type WorkflowSidebarBodyProps,
6+
} from './workflow-sidebar'

0 commit comments

Comments
 (0)