From 1e1268f0ec63bfdc74065813029544aa84ed649e Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 22 May 2026 17:58:16 -0700 Subject: [PATCH 01/19] feat(tables): native enrichments sidebar + workflow input mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../app/api/table/[tableId]/groups/route.ts | 4 + .../enrichments-sidebar.tsx | 227 +++++++++++++++++ .../components/enrichments-sidebar/index.ts | 1 + .../tables/[tableId]/components/index.ts | 1 + .../new-column-dropdown.tsx | 17 +- .../components/table-grid/table-grid.tsx | 6 +- .../components/workflow-sidebar/index.ts | 7 +- .../input-mapping-section.tsx | 109 ++++++++ .../workflow-sidebar/workflow-sidebar.tsx | 235 ++++++++++++++---- .../[workspaceId]/tables/[tableId]/table.tsx | 28 ++- .../background/workflow-column-execution.ts | 13 +- apps/sim/hooks/queries/tables.ts | 2 + apps/sim/lib/api/contracts/tables.ts | 16 ++ apps/sim/lib/table/service.ts | 10 +- apps/sim/lib/table/types.ts | 27 ++ apps/sim/lib/table/workflow-columns.ts | 32 ++- 16 files changed, 662 insertions(+), 73 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/input-mapping-section.tsx diff --git a/apps/sim/app/api/table/[tableId]/groups/route.ts b/apps/sim/app/api/table/[tableId]/groups/route.ts index bf74653212a..197a1722b1b 100644 --- a/apps/sim/app/api/table/[tableId]/groups/route.ts +++ b/apps/sim/app/api/table/[tableId]/groups/route.ts @@ -113,6 +113,10 @@ export const PATCH = withRouteHandler(async (request: NextRequest, { params }: R ...(validated.mappingUpdates !== undefined ? { mappingUpdates: validated.mappingUpdates } : {}), + ...(validated.inputMappings !== undefined + ? { inputMappings: validated.inputMappings } + : {}), + ...(validated.type !== undefined ? { type: validated.type } : {}), ...(validated.autoRun !== undefined ? { autoRun: validated.autoRun } : {}), }, requestId diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx new file mode 100644 index 00000000000..b9b3818623b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx @@ -0,0 +1,227 @@ +'use client' + +import type React from 'react' +import { useState } from 'react' +import { + Briefcase, + Building2, + DollarSign, + Globe, + Link2, + Mail, + Sparkles, + TrendingUp, + X, +} from 'lucide-react' +import { Button, Input } from '@/components/emcn' +import { Search } from '@/components/emcn/icons' +import { cn } from '@/lib/core/utils/cn' +import type { ColumnDefinition, WorkflowGroup } from '@/lib/table' +import type { WorkflowMetadata } from '@/stores/workflows/registry/types' +import { generateColumnName } from '../../utils' +import { type WorkflowConfig, WorkflowSidebarBody } from '../workflow-sidebar' + +/** A shared enrichment a user can drop onto a table as a workflow column. */ +export interface EnrichmentTemplate { + id: string + name: string + description: string + icon: React.ComponentType<{ className?: string }> +} + +/** + * Curated catalog shown in the enrichments list. Stand-in data until a real + * shared-workflow catalog exists — every card currently resolves to the first + * workspace workflow as its template (see `Table`'s `onPickEnrichment`). + */ +const ENRICHMENT_TEMPLATES: EnrichmentTemplate[] = [ + { + id: 'use-ai', + name: 'Use AI', + description: 'Run a custom AI prompt over each row.', + icon: Sparkles, + }, + { id: 'work-email', name: 'Work Email', description: "Find a person's work email.", icon: Mail }, + { + id: 'company-domain', + name: 'Company Domain', + description: 'Find a domain address from a company name.', + icon: Link2, + }, + { + id: 'website-traffic', + name: 'Website Traffic (Monthly)', + description: 'Get the monthly website traffic for a domain.', + icon: TrendingUp, + }, + { + id: 'company-funding', + name: 'Company Latest Funding', + description: "Look up a company's latest funding details.", + icon: DollarSign, + }, + { + id: 'website-techstack', + name: 'Website Techstack', + description: 'See what technologies a website uses.', + icon: Globe, + }, + { + id: 'company-revenue', + name: 'Company Revenue', + description: "Find a company's revenue.", + icon: Building2, + }, + { + id: 'company-jobs', + name: 'Company Job Openings', + description: "Look up a company's current job openings.", + icon: Briefcase, + }, +] + +interface EnrichmentsSidebarProps { + open: boolean + onClose: () => void + /** Forwarded to the hosted workflow body — same props `WorkflowSidebar` takes. */ + allColumns: ColumnDefinition[] + workflowGroups: WorkflowGroup[] + workflows: WorkflowMetadata[] | undefined + workspaceId: string + tableId: string + onColumnRename?: (oldName: string, newName: string) => void +} + +/** + * Right-edge panel for the enrichments flow. Hosts both the catalog list and + * (once a card is picked) the workflow-config body in the *same* sliding panel, + * so picking an enrichment swaps content in place rather than cross-sliding a + * second panel over the list. + */ +export function EnrichmentsSidebar({ open, ...rest }: EnrichmentsSidebarProps) { + return ( + + ) +} + +function EnrichmentsSidebarBody({ + onClose, + allColumns, + workflowGroups, + workflows, + workspaceId, + tableId, + onColumnRename, +}: Omit) { + const [selected, setSelected] = useState(null) + const [query, setQuery] = useState('') + + // A card is picked — show the workflow-config body in this same panel. The + // `key` remounts the body when the selection (or its resolved workflow) + // changes so its form state re-seeds. + if (selected) { + const workflowId = workflows?.[0]?.id + const config: WorkflowConfig = { + mode: 'create', + kind: 'enrichment', + proposedName: generateColumnName(allColumns), + workflowId, + enrichmentName: selected.name, + } + return ( + setSelected(null)} + /> + ) + } + + const normalized = query.trim().toLowerCase() + const filtered = normalized + ? ENRICHMENT_TEMPLATES.filter( + (t) => + t.name.toLowerCase().includes(normalized) || + t.description.toLowerCase().includes(normalized) + ) + : ENRICHMENT_TEMPLATES + + return ( +
+
+

Enrichments

+ +
+ +
+
+ + setQuery(e.target.value)} + placeholder='Search' + spellCheck={false} + autoComplete='off' + className='pl-7' + /> +
+
+ +
+ {filtered.length === 0 ? ( +

No enrichments found.

+ ) : ( +
    + {filtered.map((template) => { + const Icon = template.icon + return ( +
  • + +
  • + ) + })} +
+ )} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/index.ts new file mode 100644 index 00000000000..8875d0c2b35 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/index.ts @@ -0,0 +1 @@ +export { EnrichmentsSidebar, type EnrichmentTemplate } from './enrichments-sidebar' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts index 0fca186c0c6..34b5f41f5fa 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts @@ -1,5 +1,6 @@ export * from './column-config-sidebar' export * from './context-menu' +export * from './enrichments-sidebar' export * from './new-column-dropdown' export * from './row-modal' export * from './run-status-control' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx index 32956f7c6ca..045e44390f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx @@ -1,10 +1,12 @@ 'use client' +import { Sparkles } from 'lucide-react' import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/emcn' import { Plus } from '@/components/emcn/icons' @@ -28,18 +30,20 @@ interface NewColumnDropdownProps { disabled: boolean onPickType: (type: ColumnDefinition['type']) => void onPickWorkflow: () => void + onPickEnrichment: () => void } /** * "+ New column" dropdown — the single entry point for creating a column. - * Lists every column type plus "Workflow"; picking a type opens the right - * sidebar pre-seeded. + * Lists every column type plus "Workflow" and "Enrichments"; picking a type + * opens the right sidebar pre-seeded. */ export function NewColumnDropdown({ trigger, disabled, onPickType, onPickWorkflow, + onPickEnrichment, }: NewColumnDropdownProps) { const menu = ( @@ -61,6 +65,15 @@ export function NewColumnDropdown({ )} + {isWorkflowColumnsEnabledClient && ( + <> + + + Enrichments + + + + )} {VISIBLE_COLUMN_TYPE_OPTIONS.map((option) => { const Icon = option.icon const onSelect = diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index 3b8dab0c1b8..64538505765 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -139,6 +139,8 @@ interface TableGridProps { */ onOpenColumnConfig: (cfg: ColumnConfig) => void onOpenWorkflowConfig: (cfg: WorkflowConfig) => void + /** Open the enrichments list (Clay-style catalog) slideout. */ + onOpenEnrichments: () => void onOpenExecutionDetails: (executionId: string) => void /** Open the row-edit modal for `row`. Wrapper renders the modal. */ onOpenRowModal: (row: TableRowType) => void @@ -243,6 +245,7 @@ export function TableGrid({ sidebarReservedPx, onOpenColumnConfig, onOpenWorkflowConfig, + onOpenEnrichments, onOpenExecutionDetails, onOpenRowModal, onRequestDeleteRows, @@ -2560,7 +2563,7 @@ export function TableGrid({ /** Open the workflow-config sidebar to spawn a brand-new workflow group. */ function handleAddWorkflowColumn() { - onOpenWorkflowConfig({ mode: 'create', proposedName: generateColumnName() }) + onOpenWorkflowConfig({ mode: 'create', kind: 'manual', proposedName: generateColumnName() }) } const handleConfigureColumn = useCallback( @@ -3229,6 +3232,7 @@ export function TableGrid({ disabled={addColumnMutation.isPending} onPickType={handleAddColumnOfType} onPickWorkflow={handleAddWorkflowColumn} + onPickEnrichment={onOpenEnrichments} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/index.ts index 6d45862e281..b6ea2bc1a86 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/index.ts @@ -1 +1,6 @@ -export { type WorkflowConfig, WorkflowSidebar } from './workflow-sidebar' +export { + type WorkflowConfig, + WorkflowSidebar, + WorkflowSidebarBody, + type WorkflowSidebarBodyProps, +} from './workflow-sidebar' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/input-mapping-section.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/input-mapping-section.tsx new file mode 100644 index 00000000000..36154fc810a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/input-mapping-section.tsx @@ -0,0 +1,109 @@ +'use client' + +import { useState } from 'react' +import { Badge, Combobox, Label } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { handleKeyboardActivation } from '@/lib/core/utils/keyboard' +import type { ColumnDefinition } from '@/lib/table' +import type { InputFormatField } from '@/lib/workflows/types' + +interface InputMappingSectionProps { + /** The workflow Start block's input fields. Each gets one collapsible row. */ + inputFields: InputFormatField[] + /** Columns the user can feed into an input (all table columns). */ + columnOptions: ColumnDefinition[] + /** Current mapping: input field name → table column name. */ + value: Record + onChange: (next: Record) => void +} + +/** + * "Workflow inputs" panel: maps each of the workflow's Start-block input fields + * to the table column whose per-row value feeds it. Each field renders as a + * collapsible card — header shows the field name + type badge, the body holds + * the column picker — mirroring the workflow editor's input-mapping rows. + */ +export function InputMappingSection({ + inputFields, + columnOptions, + value, + onChange, +}: InputMappingSectionProps) { + const namedFields = inputFields.filter((f): f is InputFormatField & { name: string } => + Boolean(f.name?.trim()) + ) + const columns = columnOptions.map((c) => ({ label: c.name, value: c.name })) + const [collapsed, setCollapsed] = useState>({}) + + const toggle = (name: string) => setCollapsed((prev) => ({ ...prev, [name]: !prev[name] })) + + return ( +
+ + {namedFields.length === 0 ? ( +

+ This workflow has no Start block inputs. +

+ ) : ( +
+ {namedFields.map((field) => { + const isCollapsed = collapsed[field.name] ?? false + return ( +
+
toggle(field.name)} + onKeyDown={(event) => handleKeyboardActivation(event, () => toggle(field.name))} + > +
+ + {field.name} + + {field.type && ( + + {field.type} + + )} +
+
+ + {!isCollapsed && ( +
+ + + onChange({ ...value, [field.name]: columnName }) + } + /> +
+ )} +
+ ) + })} +
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx index b339d3397ef..4785764171a 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx @@ -18,6 +18,7 @@ import { Tooltip, toast, } from '@/components/emcn' +import { ArrowLeft, ChevronDown } from '@/components/emcn/icons' import { findValidationIssue, isValidationError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' import type { @@ -33,6 +34,7 @@ import type { ColumnDefinition, WorkflowGroup, WorkflowGroupDependencies, + WorkflowGroupInputMapping, WorkflowGroupOutput, } from '@/lib/table' import { columnTypeForLeaf, deriveOutputColumnName } from '@/lib/table/column-naming' @@ -54,11 +56,22 @@ import { } from '@/hooks/queries/tables' import { useWorkflowState, workflowKeys } from '@/hooks/queries/workflows' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' +import { InputMappingSection } from './input-mapping-section' import { RunSettingsSection } from './run-settings-section' +/** + * Distinguishes a user-built workflow column (`manual`) from one spawned off a + * shared enrichment template (`enrichment`). Enrichment groups hide the + * launch-workflow and add-inputs affordances and surface a back button to the + * enrichments list. + */ +export type WorkflowSidebarKind = 'manual' | 'enrichment' + /** * Discriminates the three flows the workflow sidebar handles: - * - `create`: brand-new workflow group spawned from the "+ New column" dropdown's "Workflow" item. + * - `create`: brand-new workflow group. From the "+ New column" dropdown's "Workflow" item + * (`kind: 'manual'`) or from an enrichment card (`kind: 'enrichment'`, with the template's + * workflow pre-seeded). * - `edit-group`: opened from the workflow-group meta header. Lets the user edit the whole group * (workflow id, deps, output set, group name). * - `edit-output`: opened from a single workflow-output column header. Focuses on this column's @@ -66,7 +79,15 @@ import { RunSettingsSection } from './run-settings-section' * secondary. */ export type WorkflowConfig = - | { mode: 'create'; proposedName: string } + | { + mode: 'create' + kind: WorkflowSidebarKind + proposedName: string + /** Pre-selected (and locked) workflow id for enrichment-create. */ + workflowId?: string + /** Title shown for enrichment-create (the enrichment card's name). */ + enrichmentName?: string + } | { mode: 'edit-group'; groupId: string } | { mode: 'edit-output'; columnName: string } @@ -83,8 +104,18 @@ interface WorkflowSidebarProps { /** Notify parent of a per-output-column rename so it can rewrite local * `columnOrder` / `columnWidths` keys. */ onColumnRename?: (oldName: string, newName: string) => void + /** When set and the active config is an enrichment, renders a back button + * that returns to the enrichments list. */ + onBack?: () => void } +/** Dashed hairline flanking the "Show additional fields" disclosure — mirrors + * the workflow editor's advanced-mode divider. */ +const DASHED_DIVIDER_STYLE = { + backgroundImage: + 'repeating-linear-gradient(to right, var(--border) 0px, var(--border) 6px, transparent 6px, transparent 12px)', +} as const + const OUTPUT_VALUE_SEPARATOR = '::' const encodeOutputValue = (blockId: string, path: string) => @@ -197,7 +228,7 @@ export function WorkflowSidebar(props: WorkflowSidebarProps) { function configKey(config: WorkflowConfig): string { switch (config.mode) { case 'create': - return `create:${config.proposedName}` + return `create:${config.kind}:${config.workflowId ?? ''}:${config.proposedName}` case 'edit-group': return `edit-group:${config.groupId}` case 'edit-output': @@ -205,11 +236,17 @@ function configKey(config: WorkflowConfig): string { } } -interface WorkflowSidebarBodyProps extends Omit { +export interface WorkflowSidebarBodyProps extends Omit { config: WorkflowConfig } -function WorkflowSidebarBody({ +/** + * The sidebar's inner content (header + scrollable form + footer) without the + * sliding `