@@ -25,13 +25,15 @@ import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
2525import type { InputFormatField } from '@/lib/workflows/types'
2626import { getBlock } from '@/blocks'
2727import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview'
28+ import { useDeploymentInfo , useDeployWorkflow } from '@/hooks/queries/deployments'
2829import {
2930 useAddTableColumn ,
3031 useAddWorkflowGroup ,
3132 useUpdateColumn ,
3233 useUpdateWorkflowGroup ,
3334} from '@/hooks/queries/tables'
3435import { useWorkflowState , workflowKeys } from '@/hooks/queries/workflows'
36+ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
3537import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
3638import { COLUMN_TYPE_OPTIONS , type SidebarColumnType } from './column-types'
3739
@@ -224,6 +226,44 @@ function FieldError({ message }: { message: string }) {
224226 return < p className = 'pl-0.5 text-destructive text-caption' > { message } </ p >
225227}
226228
229+ /**
230+ * Tinted inline warning row with a message on the left and an action button
231+ * on the right. Stacks naturally — render multiple in sequence and they line
232+ * up. Color mirrors the group-header deploy badge: `red` for blocking states,
233+ * `amber` for soft warnings.
234+ */
235+ function WarningRow ( {
236+ tone,
237+ message,
238+ action,
239+ } : {
240+ tone : 'red' | 'amber'
241+ message : string
242+ action : React . ReactNode
243+ } ) {
244+ return (
245+ < div
246+ className = { cn (
247+ 'flex items-center gap-2 rounded-md border px-2.5 py-2' ,
248+ tone === 'red'
249+ ? 'border-destructive/40 bg-destructive/5'
250+ : 'border-amber-500/40 bg-amber-500/5'
251+ ) }
252+ role = 'status'
253+ >
254+ < span
255+ className = { cn (
256+ 'min-w-0 flex-1 text-caption' ,
257+ tone === 'red' ? 'text-destructive' : 'text-amber-700 dark:text-amber-400'
258+ ) }
259+ >
260+ { message }
261+ </ span >
262+ < div className = 'shrink-0' > { action } </ div >
263+ </ div >
264+ )
265+ }
266+
227267/**
228268 * Right-edge configuration panel for any column.
229269 *
@@ -274,6 +314,15 @@ export function ColumnSidebar({
274314 const isWorkflow =
275315 ! ! existingGroup || configState ?. mode === 'new' || typeInput === 'workflow'
276316
317+ /**
318+ * Show the Column name field whenever a *specific* column is open: scalar
319+ * columns (create or edit) and per-output workflow columns (edit only). Hide
320+ * it when the surface is the workflow-group as a whole — i.e. creating a
321+ * brand-new workflow column where individual output names are auto-derived.
322+ */
323+ const showColumnNameField =
324+ ! isWorkflow || configState ?. mode === 'edit' || configState ?. mode === 'new'
325+
277326 /**
278327 * Columns to the left of the current column — these are the only valid trigger
279328 * dependencies, since a workflow column can't depend on values that haven't been
@@ -413,6 +462,26 @@ export function ColumnSidebar({
413462 open && isWorkflow && selectedWorkflowId ? selectedWorkflowId : undefined
414463 )
415464
465+ /**
466+ * Deployment status of the picked workflow. The sidebar surfaces an inline
467+ * warning row offering one-click (re)deploy, mirroring the badge in the
468+ * group's column header so the user sees the same signal where they're
469+ * configuring the column.
470+ */
471+ const deploymentInfo = useDeploymentInfo (
472+ open && isWorkflow && selectedWorkflowId ? selectedWorkflowId : null ,
473+ { refetchOnMount : 'always' }
474+ )
475+ const deployState : 'undeployed' | 'redeploy' | null = deploymentInfo . data
476+ ? ! deploymentInfo . data . isDeployed
477+ ? 'undeployed'
478+ : deploymentInfo . data . needsRedeployment
479+ ? 'redeploy'
480+ : null
481+ : null
482+ const { mutate : deployWorkflow , isPending : isDeploying } = useDeployWorkflow ( )
483+ const userPermissions = useUserPermissionsContext ( )
484+
416485 /**
417486 * Resolves the unified Start block id and its current `inputFormat` field
418487 * names. The "Add inputs" mutation only adds rows for table columns that
@@ -672,7 +741,13 @@ export function ColumnSidebar({
672741 if ( ! configState ) return
673742 setSaveError ( null )
674743 const trimmedName = nameInput . trim ( )
675- if ( ! trimmedName || ( isWorkflow && ( ! selectedWorkflowId || selectedOutputs . length === 0 ) ) ) {
744+ // Name is required iff the field is shown — when configuring a whole
745+ // workflow group at creation time, per-output column names are auto-derived
746+ // and the field is hidden, so don't gate save on it.
747+ if (
748+ ( showColumnNameField && ! trimmedName ) ||
749+ ( isWorkflow && ( ! selectedWorkflowId || selectedOutputs . length === 0 ) )
750+ ) {
676751 setShowValidation ( true )
677752 return
678753 }
@@ -688,8 +763,23 @@ export function ColumnSidebar({
688763 if ( existingGroup ) {
689764 // Update path: diff outputs, derive new column names for added entries,
690765 // call updateWorkflowGroup so service handles add/remove transactionally.
766+ // If the sidebar was opened on a *specific* workflow-output column and
767+ // the user renamed it, propagate that into the group's `outputs` ref
768+ // (the column rename itself goes through `updateColumn` below, which
769+ // server-side cascades into outputs/deps — but our outgoing payload
770+ // also has to use the new name so the group update doesn't undo it).
771+ const editedColumnName =
772+ configState . mode === 'edit' ? configState . columnName : null
773+ const renamedColumn =
774+ editedColumnName && trimmedName && trimmedName !== editedColumnName
775+ ? { from : editedColumnName , to : trimmedName }
776+ : null
691777 const oldKeys = new Set ( existingGroup . outputs . map ( ( o ) => `${ o . blockId } ::${ o . path } ` ) )
692- const taken = new Set ( allColumns . map ( ( c ) => c . name ) )
778+ const taken = new Set (
779+ allColumns . map ( ( c ) =>
780+ renamedColumn && c . name === renamedColumn . from ? renamedColumn . to : c . name
781+ )
782+ )
693783 const fullOutputs : WorkflowGroupOutput [ ] = [ ]
694784 const newOutputColumns : ColumnDefinition [ ] = [ ]
695785 for ( const o of orderedOutputs ) {
@@ -698,7 +788,11 @@ export function ColumnSidebar({
698788 ( e ) => e . blockId === o . blockId && e . path === o . path
699789 )
700790 if ( existing ) {
701- fullOutputs . push ( existing )
791+ fullOutputs . push (
792+ renamedColumn && existing . columnName === renamedColumn . from
793+ ? { ...existing , columnName : renamedColumn . to }
794+ : existing
795+ )
702796 } else {
703797 const blockName = blockNameByBlockId . get ( o . blockId ) ?? 'output'
704798 const colName = deriveOutputColumnName ( blockName , o . path , taken )
@@ -714,6 +808,12 @@ export function ColumnSidebar({
714808 }
715809 oldKeys . delete ( key )
716810 }
811+ if ( renamedColumn ) {
812+ await updateColumn . mutateAsync ( {
813+ columnName : renamedColumn . from ,
814+ updates : { name : renamedColumn . to } ,
815+ } )
816+ }
717817 await updateWorkflowGroup . mutateAsync ( {
718818 groupId : existingGroup . id ,
719819 workflowId : selectedWorkflowId ,
@@ -835,24 +935,28 @@ export function ColumnSidebar({
835935 />
836936 </ div >
837937
838- < FieldDivider />
938+ { showColumnNameField && (
939+ < >
940+ < FieldDivider />
839941
840- < div className = 'flex flex-col gap-[9.5px]' >
841- < FieldLabel htmlFor = 'column-sidebar-name' required >
842- Column name
843- </ FieldLabel >
844- < Input
845- id = 'column-sidebar-name'
846- value = { nameInput }
847- onChange = { ( e ) => setNameInput ( e . target . value ) }
848- spellCheck = { false }
849- autoComplete = 'off'
850- aria-invalid = { showValidation && ! nameInput . trim ( ) ? true : undefined }
851- />
852- { showValidation && ! nameInput . trim ( ) && (
853- < FieldError message = 'Column name is required' />
854- ) }
855- </ div >
942+ < div className = 'flex flex-col gap-[9.5px]' >
943+ < FieldLabel htmlFor = 'column-sidebar-name' required >
944+ Column name
945+ </ FieldLabel >
946+ < Input
947+ id = 'column-sidebar-name'
948+ value = { nameInput }
949+ onChange = { ( e ) => setNameInput ( e . target . value ) }
950+ spellCheck = { false }
951+ autoComplete = 'off'
952+ aria-invalid = { showValidation && ! nameInput . trim ( ) ? true : undefined }
953+ />
954+ { showValidation && ! nameInput . trim ( ) && (
955+ < FieldError message = 'Column name is required' />
956+ ) }
957+ </ div >
958+ </ >
959+ ) }
856960
857961 { ! isWorkflow && (
858962 < >
@@ -955,27 +1059,70 @@ export function ColumnSidebar({
9551059 { selectedWorkflowId &&
9561060 startBlockInputs . blockId &&
9571061 missingInputColumnNames . length > 0 && (
958- < Tooltip . Root >
959- < Tooltip . Trigger asChild >
960- < Button
961- type = 'button'
962- variant = 'default'
963- size = 'sm'
964- onClick = { ( ) => addInputsMutation . mutate ( ) }
965- disabled = { addInputsMutation . isPending }
966- className = 'self-start'
967- >
968- < Plus className = 'h-[14px] w-[14px]' />
969- { addInputsMutation . isPending
970- ? 'Adding…'
971- : `Add inputs (${ missingInputColumnNames . length } )` }
972- </ Button >
973- </ Tooltip . Trigger >
974- < Tooltip . Content side = 'top' >
975- Adds { missingInputColumnNames . join ( ', ' ) } to the workflow's Start block
976- </ Tooltip . Content >
977- </ Tooltip . Root >
1062+ < WarningRow
1063+ tone = 'amber'
1064+ message = { `Start block missing ${ missingInputColumnNames . length } table input${
1065+ missingInputColumnNames . length === 1 ? '' : 's'
1066+ } `}
1067+ action = {
1068+ < Tooltip . Root >
1069+ < Tooltip . Trigger asChild >
1070+ < Button
1071+ type = 'button'
1072+ variant = 'default'
1073+ size = 'sm'
1074+ onClick = { ( ) => addInputsMutation . mutate ( ) }
1075+ disabled = { addInputsMutation . isPending }
1076+ >
1077+ < Plus className = 'h-[14px] w-[14px]' />
1078+ { addInputsMutation . isPending
1079+ ? 'Adding…'
1080+ : `Add inputs (${ missingInputColumnNames . length } )` }
1081+ </ Button >
1082+ </ Tooltip . Trigger >
1083+ < Tooltip . Content side = 'top' >
1084+ Adds { missingInputColumnNames . join ( ', ' ) } to the workflow's Start block
1085+ </ Tooltip . Content >
1086+ </ Tooltip . Root >
1087+ }
1088+ />
9781089 ) }
1090+ { selectedWorkflowId && deployState && (
1091+ < WarningRow
1092+ tone = { deployState === 'undeployed' ? 'red' : 'amber' }
1093+ message = {
1094+ deployState === 'undeployed'
1095+ ? 'Workflow is not deployed'
1096+ : 'Workflow has changes since last deploy'
1097+ }
1098+ action = {
1099+ < Tooltip . Root >
1100+ < Tooltip . Trigger asChild >
1101+ < Button
1102+ type = 'button'
1103+ variant = 'default'
1104+ size = 'sm'
1105+ onClick = { ( ) => deployWorkflow ( { workflowId : selectedWorkflowId } ) }
1106+ disabled = { isDeploying || ! userPermissions . canAdmin }
1107+ >
1108+ { isDeploying
1109+ ? 'Deploying…'
1110+ : deployState === 'undeployed'
1111+ ? 'Deploy'
1112+ : 'Redeploy' }
1113+ </ Button >
1114+ </ Tooltip . Trigger >
1115+ < Tooltip . Content side = 'top' >
1116+ { ! userPermissions . canAdmin
1117+ ? 'Admin permission required to deploy'
1118+ : deployState === 'undeployed'
1119+ ? 'Deploy this workflow'
1120+ : 'Redeploy with the latest changes' }
1121+ </ Tooltip . Content >
1122+ </ Tooltip . Root >
1123+ }
1124+ />
1125+ ) }
9791126 </ div >
9801127
9811128 < FieldDivider />
0 commit comments