Skip to content

Commit 69ac23d

Browse files
fix table column swapping behavior
1 parent 44688c4 commit 69ac23d

1 file changed

Lines changed: 133 additions & 29 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx

Lines changed: 133 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)