Skip to content

Commit bd68516

Browse files
feat(tables): present enrichment columns as first-class in the grid
- Meta-header shows the enrichment's name + icon (Mail/Phone/Globe/Building2) instead of "Workflow" + a color chip. - Per-column header icon uses the enrichment's icon (via columnSourceInfo) instead of the generic play icon. - Hide "View execution" for enrichment cells in both the row context menu and the action bar (no workflow execution exists to open); also hide the meta-menu "View workflow" item for enrichment groups. - Clicking an enrichment column header now opens the enrichments sidebar in edit mode (pre-filled input mappings, Update via useUpdateWorkflowGroup) instead of the workflow "Configure workflow" sidebar. - Enrichment config lets the user name each output column (editable per-output, deduped defaults) since enrichments can produce multiple columns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent df8fe6e commit bd68516

6 files changed

Lines changed: 229 additions & 36 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ import {
1010
CollapsibleCard,
1111
Combobox,
1212
FieldDivider,
13+
Input,
1314
Label,
1415
Switch,
1516
toast,
1617
} from '@/components/emcn'
1718
import { ArrowLeft } from '@/components/emcn/icons'
1819
import type { AddWorkflowGroupBodyInput } from '@/lib/api/contracts/tables'
20+
import { cn } from '@/lib/core/utils/cn'
1921
import type { ColumnDefinition, WorkflowGroup, WorkflowGroupOutput } from '@/lib/table'
2022
import { deriveOutputColumnName } from '@/lib/table/column-naming'
2123
import type { EnrichmentConfig as EnrichmentDef } from '@/enrichments/types'
22-
import { useAddWorkflowGroup } from '@/hooks/queries/tables'
24+
import { useAddWorkflowGroup, useUpdateWorkflowGroup } from '@/hooks/queries/tables'
2325
import { RunSettingsSection } from '../workflow-sidebar/run-settings-section'
2426

2527
interface EnrichmentConfigProps {
@@ -29,6 +31,10 @@ interface EnrichmentConfigProps {
2931
tableId: string
3032
onBack: () => void
3133
onClose: () => void
34+
/** When set, the panel edits this existing enrichment group (pre-filled,
35+
* updates instead of creating). Output columns already exist and are shown
36+
* read-only. */
37+
existingGroup?: WorkflowGroup
3238
}
3339

3440
/** Pre-fill an input's column from a same-named column (case-insensitive). */
@@ -56,38 +62,102 @@ export function EnrichmentConfig({
5662
tableId,
5763
onBack,
5864
onClose,
65+
existingGroup,
5966
}: EnrichmentConfigProps) {
6067
const addWorkflowGroup = useAddWorkflowGroup({ workspaceId, tableId })
68+
const updateWorkflowGroup = useUpdateWorkflowGroup({ workspaceId, tableId })
69+
const isEditing = Boolean(existingGroup)
6170

6271
const [inputMappings, setInputMappings] = useState<Record<string, string>>(() => {
72+
if (existingGroup) {
73+
const seed: Record<string, string> = {}
74+
for (const m of existingGroup.inputMappings ?? []) seed[m.inputName] = m.columnName
75+
return seed
76+
}
6377
const seed: Record<string, string> = {}
6478
for (const input of enrichment.inputs) {
6579
const col = defaultColumnFor(input, allColumns)
6680
if (col) seed[input.id] = col
6781
}
6882
return seed
6983
})
84+
// Per-output column names. Edit mode reflects the existing columns (read-only);
85+
// create mode seeds deduped defaults the user can rename.
86+
const [outputNames, setOutputNames] = useState<Record<string, string>>(() => {
87+
const seed: Record<string, string> = {}
88+
if (existingGroup) {
89+
for (const o of existingGroup.outputs) {
90+
if (o.outputId) seed[o.outputId] = o.columnName
91+
}
92+
return seed
93+
}
94+
const taken = new Set(allColumns.map((c) => c.name))
95+
for (const o of enrichment.outputs) {
96+
const colName = deriveOutputColumnName(o.name, taken)
97+
taken.add(colName)
98+
seed[o.id] = colName
99+
}
100+
return seed
101+
})
70102
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
71-
const [autoRun, setAutoRun] = useState(false)
72-
const [deps, setDeps] = useState<string[]>([])
103+
const [autoRun, setAutoRun] = useState(() => existingGroup?.autoRun ?? false)
104+
const [deps, setDeps] = useState<string[]>(() => existingGroup?.dependencies?.columns ?? [])
73105
const [showValidation, setShowValidation] = useState(false)
74106

75107
const columnOptions = allColumns.map((c) => ({ label: c.name, value: c.name }))
76108
const missingRequired = enrichment.inputs.some((i) => i.required && !inputMappings[i.id])
77109
const depsValid = !autoRun || deps.length > 0
78-
const saveDisabled = addWorkflowGroup.isPending || !depsValid
110+
111+
/** Per-output name validation (create mode only — edit mode columns exist). */
112+
function outputNameError(outputId: string): string | null {
113+
if (isEditing) return null
114+
const value = (outputNames[outputId] ?? '').trim()
115+
if (!value) return 'Required'
116+
const lower = value.toLowerCase()
117+
if (allColumns.some((c) => c.name.toLowerCase() === lower)) return 'Column already exists'
118+
const dup = enrichment.outputs.some(
119+
(o) => o.id !== outputId && (outputNames[o.id] ?? '').trim().toLowerCase() === lower
120+
)
121+
return dup ? 'Duplicate name' : null
122+
}
123+
const outputsInvalid =
124+
!isEditing && enrichment.outputs.some((o) => outputNameError(o.id) !== null)
125+
const saveDisabled =
126+
addWorkflowGroup.isPending || updateWorkflowGroup.isPending || !depsValid || outputsInvalid
79127

80128
async function handleSave() {
81-
if (missingRequired || (autoRun && deps.length === 0)) {
129+
if (missingRequired || (autoRun && deps.length === 0) || outputsInvalid) {
82130
setShowValidation(true)
83131
return
84132
}
133+
const inputMappingsList = Object.entries(inputMappings)
134+
.filter(([, columnName]) => Boolean(columnName))
135+
.map(([inputName, columnName]) => ({ inputName, columnName }))
136+
137+
if (existingGroup) {
138+
try {
139+
await updateWorkflowGroup.mutateAsync({
140+
groupId: existingGroup.id,
141+
name: enrichment.name,
142+
dependencies: { columns: deps },
143+
inputMappings: inputMappingsList,
144+
autoRun,
145+
})
146+
toast.success(`Updated "${enrichment.name}"`)
147+
onClose()
148+
} catch (err) {
149+
toast.error(toError(err).message)
150+
}
151+
return
152+
}
153+
85154
const groupId = generateId()
86155
const taken = new Set(allColumns.map((c) => c.name))
87156
const outputColumns: AddWorkflowGroupBodyInput['outputColumns'] = []
88157
const outputs: WorkflowGroupOutput[] = []
89158
for (const o of enrichment.outputs) {
90-
const colName = deriveOutputColumnName(o.name, taken)
159+
const desired = (outputNames[o.id] ?? '').trim() || o.name
160+
const colName = deriveOutputColumnName(desired, taken)
91161
taken.add(colName)
92162
outputColumns.push({
93163
name: colName,
@@ -98,9 +168,6 @@ export function EnrichmentConfig({
98168
})
99169
outputs.push({ blockId: '', path: '', outputId: o.id, columnName: colName })
100170
}
101-
const inputMappingsList = Object.entries(inputMappings)
102-
.filter(([, columnName]) => Boolean(columnName))
103-
.map(([inputName, columnName]) => ({ inputName, columnName }))
104171

105172
const group: WorkflowGroup = {
106173
id: groupId,
@@ -205,9 +272,42 @@ export function EnrichmentConfig({
205272

206273
<div className='flex flex-col gap-[9.5px]'>
207274
<Label className='pl-0.5'>Output columns</Label>
208-
<p className='pl-0.5 text-[var(--text-tertiary)] text-caption'>
209-
Creates: {enrichment.outputs.map((o) => o.name).join(', ')}
210-
</p>
275+
<div className='flex flex-col gap-2'>
276+
{enrichment.outputs.map((output) => {
277+
const outErr = showValidation ? outputNameError(output.id) : null
278+
return (
279+
<CollapsibleCard
280+
key={output.id}
281+
title={output.name}
282+
badge={
283+
<Badge variant='type' size='sm'>
284+
{output.type}
285+
</Badge>
286+
}
287+
collapsed={collapsed[`out:${output.id}`] ?? false}
288+
onToggleCollapse={() =>
289+
setCollapsed((prev) => ({
290+
...prev,
291+
[`out:${output.id}`]: !prev[`out:${output.id}`],
292+
}))
293+
}
294+
>
295+
<Label className='text-small'>Column name</Label>
296+
<Input
297+
value={outputNames[output.id] ?? ''}
298+
onChange={(e) =>
299+
setOutputNames((prev) => ({ ...prev, [output.id]: e.target.value }))
300+
}
301+
disabled={isEditing}
302+
spellCheck={false}
303+
autoComplete='off'
304+
className={cn(outErr && 'border-[var(--text-error)]')}
305+
/>
306+
{outErr && <p className='text-[var(--text-error)] text-caption'>{outErr}</p>}
307+
</CollapsibleCard>
308+
)
309+
})}
310+
</div>
211311
</div>
212312

213313
<FieldDivider />
@@ -238,7 +338,7 @@ export function EnrichmentConfig({
238338
Cancel
239339
</Button>
240340
<Button variant='primary' size='sm' onClick={handleSave} disabled={saveDisabled}>
241-
{saveDisabled ? 'Saving…' : 'Save'}
341+
{isEditing ? 'Update' : 'Save'}
242342
</Button>
243343
</div>
244344
</div>

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { X } from 'lucide-react'
55
import { Button, Input } from '@/components/emcn'
66
import { Search } from '@/components/emcn/icons'
77
import { cn } from '@/lib/core/utils/cn'
8-
import type { ColumnDefinition } from '@/lib/table'
8+
import type { ColumnDefinition, WorkflowGroup } from '@/lib/table'
99
import { ALL_ENRICHMENTS } from '@/enrichments'
10+
import { getEnrichment } from '@/enrichments/registry'
1011
import type { EnrichmentConfig as EnrichmentDef } from '@/enrichments/types'
1112
import { EnrichmentConfig } from './enrichment-config'
1213

@@ -16,6 +17,9 @@ interface EnrichmentsSidebarProps {
1617
allColumns: ColumnDefinition[]
1718
workspaceId: string
1819
tableId: string
20+
/** When set, the sidebar opens straight into this enrichment group's config
21+
* in edit mode (skips the catalog list). */
22+
editGroup?: WorkflowGroup
1923
}
2024

2125
/**
@@ -43,10 +47,28 @@ function EnrichmentsSidebarBody({
4347
allColumns,
4448
workspaceId,
4549
tableId,
50+
editGroup,
4651
}: Omit<EnrichmentsSidebarProps, 'open'>) {
4752
const [selected, setSelected] = useState<EnrichmentDef | null>(null)
4853
const [query, setQuery] = useState('')
4954

55+
// Edit mode: open the picked enrichment's config directly, pre-filled from the
56+
// existing group. No catalog list / back-to-list step.
57+
const editEnrichment = editGroup ? getEnrichment(editGroup.enrichmentId) : undefined
58+
if (editGroup && editEnrichment) {
59+
return (
60+
<EnrichmentConfig
61+
enrichment={editEnrichment}
62+
existingGroup={editGroup}
63+
allColumns={allColumns}
64+
workspaceId={workspaceId}
65+
tableId={tableId}
66+
onBack={onClose}
67+
onClose={onClose}
68+
/>
69+
)
70+
}
71+
5072
if (selected) {
5173
return (
5274
<EnrichmentConfig

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
} from '@/components/emcn/icons'
2424
import type { RunMode } from '@/lib/api/contracts/tables'
2525
import { cn } from '@/lib/core/utils/cn'
26+
import type { WorkflowGroupType } from '@/lib/table'
27+
import { getEnrichment } from '@/enrichments/registry'
2628
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
2729
import { SELECTION_TINT_BG } from '../constants'
2830
import type { DisplayColumn } from '../types'
@@ -166,6 +168,13 @@ export function ColumnOptionsMenu({
166168
interface WorkflowGroupMetaCellProps {
167169
workflowId: string
168170
groupId: string
171+
/** When `'enrichment'`, the cell shows the enrichment's name + icon instead
172+
* of a backing workflow's color chip + name. */
173+
groupType?: WorkflowGroupType
174+
/** Registry id for enrichment groups (resolves name/icon fallback). */
175+
enrichmentId?: string
176+
/** Persisted group name (the enrichment name at creation). */
177+
groupName?: string
169178
size: number
170179
startColIndex: number
171180
columnName: string
@@ -205,6 +214,9 @@ interface WorkflowGroupMetaCellProps {
205214
export function WorkflowGroupMetaCell({
206215
workflowId,
207216
groupId,
217+
groupType,
218+
enrichmentId,
219+
groupName,
208220
size,
209221
startColIndex,
210222
columnName,
@@ -226,9 +238,14 @@ export function WorkflowGroupMetaCell({
226238
onDragLeave,
227239
readOnly,
228240
}: WorkflowGroupMetaCellProps) {
241+
const isEnrichment = groupType === 'enrichment'
242+
const enrichment = isEnrichment ? getEnrichment(enrichmentId) : undefined
243+
const EnrichmentIcon = enrichment?.icon
229244
const wf = workflows?.find((w) => w.id === workflowId)
230245
const color = wf?.color ?? 'var(--text-muted)'
231-
const name = wf?.name ?? 'Workflow'
246+
const name = isEnrichment
247+
? (groupName ?? enrichment?.name ?? 'Enrichment')
248+
: (wf?.name ?? 'Workflow')
232249

233250
const [optionsMenuOpen, setOptionsMenuOpen] = useState(false)
234251
const [optionsMenuPosition, setOptionsMenuPosition] = useState({ x: 0, y: 0 })
@@ -369,14 +386,18 @@ export function WorkflowGroupMetaCell({
369386
style={{ background: color }}
370387
/>
371388
<div className='flex h-[18px] min-w-0 items-center gap-1.5'>
372-
<span
373-
className='size-[10px] shrink-0 rounded-sm border-[2px]'
374-
style={{
375-
backgroundColor: color,
376-
borderColor: `${color}60`,
377-
backgroundClip: 'padding-box',
378-
}}
379-
/>
389+
{isEnrichment && EnrichmentIcon ? (
390+
<EnrichmentIcon className='size-[12px] shrink-0 text-[var(--text-icon)]' />
391+
) : (
392+
<span
393+
className='size-[10px] shrink-0 rounded-sm border-[2px]'
394+
style={{
395+
backgroundColor: color,
396+
borderColor: `${color}60`,
397+
backgroundClip: 'padding-box',
398+
}}
399+
/>
400+
)}
380401
<span className='min-w-0 truncate font-medium text-[11px] text-[var(--text-secondary)]'>
381402
{name}
382403
</span>

0 commit comments

Comments
 (0)