@@ -621,8 +621,12 @@ export function Table({
621621 const col = cols [ i ]
622622 const w = columnWidths [ col . key ] ?? COL_WIDTH
623623 if ( i === targetGroupStart ) {
624+ // Clamp `targetGroupSize` to remaining columns — the memo's deps may not
625+ // have settled in lockstep when a group shrinks (column removed) and we
626+ // can briefly read past the end of `cols`.
627+ const safeGroupSize = Math . min ( targetGroupSize , cols . length - i )
624628 let groupWidth = 0
625- for ( let j = 0 ; j < targetGroupSize ; j ++ ) {
629+ for ( let j = 0 ; j < safeGroupSize ; j ++ ) {
626630 groupWidth += columnWidths [ cols [ i + j ] . key ] ?? COL_WIDTH
627631 }
628632 const lineLeft = dropSide === 'left' ? left : left + groupWidth
@@ -1107,6 +1111,19 @@ export function Table({
11071111 } , [ ] )
11081112
11091113 const handleColumnDragOver = useCallback ( ( columnName : string , side : 'left' | 'right' ) => {
1114+ // Suppress drop targeting while hovering siblings of the dragged column's
1115+ // own group: reordering inside a group is meaningless (the group renders
1116+ // as a unit) and the chasing indicator just flickers.
1117+ const dragged = dragColumnNameRef . current
1118+ if ( dragged ) {
1119+ const cols = schemaColumnsRef . current
1120+ const draggedGid = cols . find ( ( c ) => c . name === dragged ) ?. workflowGroupId
1121+ const targetGid = cols . find ( ( c ) => c . name === columnName ) ?. workflowGroupId
1122+ if ( draggedGid && draggedGid === targetGid ) {
1123+ if ( dropTargetColumnNameRef . current !== null ) setDropTargetColumnName ( null )
1124+ return
1125+ }
1126+ }
11101127 if ( columnName === dropTargetColumnNameRef . current && side === dropSideRef . current ) return
11111128 setDropTargetColumnName ( columnName )
11121129 setDropSide ( side )
@@ -1124,28 +1141,107 @@ export function Table({
11241141 const target = dropTargetColumnNameRef . current
11251142 const side = dropSideRef . current
11261143 if ( target && dragged !== target ) {
1127- const cols = columnsRef . current
1128- const currentOrder = columnOrderRef . current ?? cols . map ( ( c ) => c . name )
1129- const fromIndex = currentOrder . indexOf ( dragged )
1130- const toIndex = currentOrder . indexOf ( target )
1131- if ( fromIndex !== - 1 && toIndex !== - 1 ) {
1132- const newOrder = currentOrder . filter ( ( n ) => n !== dragged )
1133- let insertIndex = newOrder . indexOf ( target )
1134- if ( side === 'right' ) insertIndex += 1
1135- newOrder . splice ( insertIndex , 0 , dragged )
1136- const orderChanged = newOrder . some ( ( name , i ) => currentOrder [ i ] !== name )
1137- if ( orderChanged ) {
1138- pushUndoRef . current ( {
1139- type : 'reorder-columns' ,
1140- previousOrder : currentOrder ,
1141- newOrder,
1142- } )
1143- setColumnOrder ( newOrder )
1144- updateMetadataRef . current ( {
1145- columnWidths : columnWidthsRef . current ,
1146- columnOrder : newOrder ,
1147- } )
1144+ const schemaCols = schemaColumnsRef . current
1145+ const currentOrder = columnOrderRef . current ?? schemaCols . map ( ( c ) => c . name )
1146+
1147+ // Group-aware reorder: a workflow group's outputs must stay contiguous in
1148+ // the persisted column order (`workflow-columns.ts` validates this on
1149+ // save). So we treat the entire group as the unit being moved when the
1150+ // dragged column belongs to one, and snap the drop position to the
1151+ // outside edge of any group the target belongs to.
1152+ const colByName = new Map ( schemaCols . map ( ( c ) => [ c . name , c ] ) )
1153+ const draggedGid = colByName . get ( dragged ) ?. workflowGroupId
1154+
1155+ const orderIndex = new Map < string , number > ( )
1156+ currentOrder . forEach ( ( n , i ) => orderIndex . set ( n , i ) )
1157+
1158+ // Compute the contiguous run covering the dragged column. For a plain
1159+ // column this is just [fromIndex, fromIndex]. For a group member it spans
1160+ // every sibling sharing the same workflowGroupId.
1161+ const fromIndex = orderIndex . get ( dragged ) ?? - 1
1162+ if ( fromIndex === - 1 ) {
1163+ setDragColumnName ( null )
1164+ setDropTargetColumnName ( null )
1165+ setDropSide ( 'left' )
1166+ return
1167+ }
1168+ let runStart = fromIndex
1169+ let runEnd = fromIndex
1170+ if ( draggedGid ) {
1171+ while (
1172+ runStart > 0 &&
1173+ colByName . get ( currentOrder [ runStart - 1 ] ) ?. workflowGroupId === draggedGid
1174+ ) {
1175+ runStart --
11481176 }
1177+ while (
1178+ runEnd < currentOrder . length - 1 &&
1179+ colByName . get ( currentOrder [ runEnd + 1 ] ) ?. workflowGroupId === draggedGid
1180+ ) {
1181+ runEnd ++
1182+ }
1183+ }
1184+ const movedNames = currentOrder . slice ( runStart , runEnd + 1 )
1185+
1186+ // Resolve the *anchor* index in `currentOrder` to drop next to. If the
1187+ // target belongs to a group (and not the dragged group), snap to that
1188+ // group's outer edge so we never split it.
1189+ const targetIdx = orderIndex . get ( target ) ?? - 1
1190+ if ( targetIdx === - 1 ) {
1191+ setDragColumnName ( null )
1192+ setDropTargetColumnName ( null )
1193+ setDropSide ( 'left' )
1194+ return
1195+ }
1196+ const targetGid = colByName . get ( target ) ?. workflowGroupId
1197+ let anchorStart = targetIdx
1198+ let anchorEnd = targetIdx
1199+ if ( targetGid && targetGid !== draggedGid ) {
1200+ while (
1201+ anchorStart > 0 &&
1202+ colByName . get ( currentOrder [ anchorStart - 1 ] ) ?. workflowGroupId === targetGid
1203+ ) {
1204+ anchorStart --
1205+ }
1206+ while (
1207+ anchorEnd < currentOrder . length - 1 &&
1208+ colByName . get ( currentOrder [ anchorEnd + 1 ] ) ?. workflowGroupId === targetGid
1209+ ) {
1210+ anchorEnd ++
1211+ }
1212+ }
1213+ // No-op if dropping the dragged run onto itself.
1214+ if ( anchorStart >= runStart && anchorEnd <= runEnd ) {
1215+ setDragColumnName ( null )
1216+ setDropTargetColumnName ( null )
1217+ setDropSide ( 'left' )
1218+ return
1219+ }
1220+
1221+ const remaining = currentOrder . filter ( ( _ , i ) => i < runStart || i > runEnd )
1222+ // After removing the moved run, recompute the anchor's name-based index.
1223+ const anchorName = side === 'left' ? currentOrder [ anchorStart ] : currentOrder [ anchorEnd ]
1224+ let insertIndex = remaining . indexOf ( anchorName )
1225+ if ( insertIndex === - 1 ) insertIndex = remaining . length
1226+ if ( side === 'right' ) insertIndex += 1
1227+ const newOrder = [
1228+ ...remaining . slice ( 0 , insertIndex ) ,
1229+ ...movedNames ,
1230+ ...remaining . slice ( insertIndex ) ,
1231+ ]
1232+
1233+ const orderChanged = newOrder . some ( ( name , i ) => currentOrder [ i ] !== name )
1234+ if ( orderChanged ) {
1235+ pushUndoRef . current ( {
1236+ type : 'reorder-columns' ,
1237+ previousOrder : currentOrder ,
1238+ newOrder,
1239+ } )
1240+ setColumnOrder ( newOrder )
1241+ updateMetadataRef . current ( {
1242+ columnWidths : columnWidthsRef . current ,
1243+ columnOrder : newOrder ,
1244+ } )
11491245 }
11501246 }
11511247 setDragColumnName ( null )
@@ -1169,17 +1265,25 @@ export function Table({
11691265 const cursorX = e . clientX - scrollRect . left + scrollEl . scrollLeft
11701266
11711267 const cols = columnsRef . current
1268+ const draggedGid = cols . find ( ( c ) => c . name === dragColumnNameRef . current ) ?. workflowGroupId
11721269 let left = CHECKBOX_COL_WIDTH
11731270 let i = 0
11741271 while ( i < cols . length ) {
11751272 const col = cols [ i ]
11761273 // Treat fanned-out groups as monolithic drop targets; accumulate across siblings.
1177- const groupSize = col . groupSize
1274+ // Clamp `groupSize` to remaining columns: dragover fires constantly and can
1275+ // race a column removal where the cached `groupSize` outpaces `cols.length`.
1276+ const groupSize = Math . min ( col . groupSize , cols . length - i )
11781277 let groupWidth = 0
11791278 for ( let j = 0 ; j < groupSize ; j ++ ) {
11801279 groupWidth += columnWidthsRef . current [ cols [ i + j ] . key ] ?? COL_WIDTH
11811280 }
11821281 if ( cursorX < left + groupWidth ) {
1282+ // Inside the dragged column's own group → no-op drop, no indicator.
1283+ if ( draggedGid && col . workflowGroupId === draggedGid ) {
1284+ if ( dropTargetColumnNameRef . current !== null ) setDropTargetColumnName ( null )
1285+ return
1286+ }
11831287 const midX = left + groupWidth / 2
11841288 const side = cursorX < midX ? 'left' : 'right'
11851289 if ( col . name !== dropTargetColumnNameRef . current || side !== dropSideRef . current ) {
@@ -4243,13 +4347,13 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
42434347 blockIconInfo = { blockIconInfo }
42444348 />
42454349 { column . workflowGroupId ? (
4246- < div className = 'ml-1.5 flex min-w-0 flex-col' >
4350+ < div className = 'ml-1.5 flex min-w-0 flex-1 flex- col text-left ' >
42474351 { blockName && (
4248- < span className = 'truncate text-[var(--text-tertiary)] text-caption leading-tight' >
4352+ < span className = 'block w-full min-w-0 truncate text-[var(--text-tertiary)] text-caption leading-tight' >
42494353 { blockName }
42504354 </ span >
42514355 ) }
4252- < span className = 'truncate font-medium text-[13px] text-[var(--text-primary)] leading-tight' >
4356+ < span className = 'block w-full min-w-0 truncate font-medium text-[13px] text-[var(--text-primary)] leading-tight' >
42534357 { column . headerLabel }
42544358 </ span >
42554359 </ div >
@@ -4273,13 +4377,13 @@ const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
42734377 blockIconInfo = { blockIconInfo }
42744378 />
42754379 { column . workflowGroupId ? (
4276- < div className = 'ml-1.5 flex min-w-0 flex-col items-start' >
4380+ < div className = 'ml-1.5 flex min-w-0 flex-1 flex- col items-start text-left ' >
42774381 { blockName && (
4278- < span className = 'truncate text-[10px] text-[var(--text-tertiary)] leading-tight' >
4382+ < span className = 'block w-full min-w-0 truncate text-[10px] text-[var(--text-tertiary)] leading-tight' >
42794383 { blockName }
42804384 </ span >
42814385 ) }
4282- < span className = 'truncate font-medium text-[var(--text-primary)] text-small leading-tight' >
4386+ < span className = 'block w-full min-w-0 truncate font-medium text-[var(--text-primary)] text-small leading-tight' >
42834387 { column . headerLabel }
42844388 </ span >
42854389 </ div >
0 commit comments