@@ -10,16 +10,18 @@ import {
1010 CollapsibleCard ,
1111 Combobox ,
1212 FieldDivider ,
13+ Input ,
1314 Label ,
1415 Switch ,
1516 toast ,
1617} from '@/components/emcn'
1718import { ArrowLeft } from '@/components/emcn/icons'
1819import type { AddWorkflowGroupBodyInput } from '@/lib/api/contracts/tables'
20+ import { cn } from '@/lib/core/utils/cn'
1921import type { ColumnDefinition , WorkflowGroup , WorkflowGroupOutput } from '@/lib/table'
2022import { deriveOutputColumnName } from '@/lib/table/column-naming'
2123import type { EnrichmentConfig as EnrichmentDef } from '@/enrichments/types'
22- import { useAddWorkflowGroup } from '@/hooks/queries/tables'
24+ import { useAddWorkflowGroup , useUpdateWorkflowGroup } from '@/hooks/queries/tables'
2325import { RunSettingsSection } from '../workflow-sidebar/run-settings-section'
2426
2527interface 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 >
0 commit comments