Skip to content

Commit 6598927

Browse files
waleedlatif1TheodoreSpeaksclaude
authored
feat(tables): pinned columns (#4770)
* feat(tables): freeze columns * fix(tables): sticky meta-header row for frozen workflow groups, remove dead handleChangeType * fix(tables): scope frozenOffsets dep to frozen column widths only * fix(tables): restore frozenColumns on delete-column undo/redo * fix(tables): restore useMemo for isAllRowsSelected (O(n) computation) * fix(tables): use current frozenColumns on delete-column redo, not stale snapshot * fix(tables): clean up frozenColumns on create-column undo * fix(tables): merge frozen state on delete-column undo instead of overwriting * fix(tables): add previousFrozenColumns to test fixture for delete-column action * fix(tables): skip frozen state update on delete-column redo when column was not frozen * refactor(tables): rename frozen columns to pinned, fix sticky-zone UX - rename frozenColumns → pinnedColumns across types, contract, undo actions, grid state/refs/props, and dropdown labels - add Pin / PinOff emcn icons; use them in the column menu in place of Lock / Unlock - pinned body cells render at z-[6], above the cell selection border (z-[5]), so the blue selection border can't draw on top of the sticky-left zone - restrict column drag-reorder to within the pinned or unpinned zone in both handleColumnDragOver and handleScrollDragOver; cross-zone drop indicators are suppressed - on unpin, slide the column to the first unpinned slot so the sticky zone stays contiguous; consolidates pin and unpin into one branch that always re-enforces pinned-at-front Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tables): biome formatting + tighten pinned-zone comments - collapse two onPinToggle JSX props that biome wanted on a single line - drop a WHAT comment in handleScrollDragOver; tighten the why-comments in handlePinToggle, handleColumnDragOver, and handleColumnDragEnd so they describe the invariant being protected instead of narrating the recent change Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tables): re-sort reorder-columns undo to keep pinned-at-front If the user reordered, then pinned a column, then undid the reorder, the restored snapshot could leave a currently-pinned column in the middle of columnOrder. pinnedOffsets walks displayColumns left→right and assigns sticky `left` from checkboxColWidth — a pinned column in the middle gets a sticky offset as if it were at the front, causing it to jump over its left neighbors on horizontal scroll. Re-sort the restored order with pinned entries pulled to the front before applying. Mirrors the belt-and-suspenders re-sort in handleColumnDragEnd. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Theodore Li <theo@sim.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 066cd70 commit 6598927

12 files changed

Lines changed: 568 additions & 226 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export interface DataRowProps {
5757
* queued indicators across page refresh during long Run-all dispatches.
5858
*/
5959
activeDispatches: ActiveDispatch[] | undefined
60+
/** Pixel `left` value for each pinned column key; absent keys are not pinned. */
61+
pinnedOffsets?: Map<string, number>
62+
/** Key of the rightmost pinned column, used to render a separator shadow. */
63+
lastPinnedColKey?: string | null
6064
}
6165

6266
function cellRangeRowChanged(
@@ -113,7 +117,9 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
113117
prev.onStopRow !== next.onStopRow ||
114118
prev.onRunRow !== next.onRunRow ||
115119
prev.workflowGroups !== next.workflowGroups ||
116-
prev.activeDispatches !== next.activeDispatches
120+
prev.activeDispatches !== next.activeDispatches ||
121+
prev.pinnedOffsets !== next.pinnedOffsets ||
122+
prev.lastPinnedColKey !== next.lastPinnedColKey
117123
) {
118124
return false
119125
}
@@ -157,6 +163,8 @@ export const DataRow = React.memo(function DataRow({
157163
onRunRow,
158164
workflowGroups,
159165
activeDispatches,
166+
pinnedOffsets,
167+
lastPinnedColKey,
160168
}: DataRowProps) {
161169
const sel = normalizedSelection
162170
/**
@@ -264,13 +272,23 @@ export const DataRow = React.memo(function DataRow({
264272
const isLeftEdge = inRange ? colIndex === sel!.startCol : colIndex === 0
265273
const isRightEdge = inRange ? colIndex === sel!.endCol : colIndex === columns.length - 1
266274

275+
const pinnedLeft = pinnedOffsets?.get(column.key)
276+
const isPinnedCell = pinnedLeft !== undefined
277+
const isPinnedSeparator = column.key === lastPinnedColKey
278+
267279
return (
268280
<td
269281
key={column.key}
270282
data-row={rowIndex}
271283
data-row-id={row.id}
272284
data-col={colIndex}
273-
className={cn(CELL, (isHighlighted || isAnchor || isEditing) && 'relative')}
285+
className={cn(
286+
CELL,
287+
(isHighlighted || isAnchor || isEditing) && 'relative',
288+
isPinnedCell && 'z-[6] bg-[var(--bg)]',
289+
isPinnedSeparator && '[box-shadow:2px_0_0_0_var(--border)]'
290+
)}
291+
style={isPinnedCell ? { position: 'sticky', left: pinnedLeft } : undefined}
274292
onMouseDown={(e) => {
275293
if (e.button !== 0 || isEditing) return
276294
onCellMouseDown(rowIndex, colIndex, e.shiftKey)

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use client'
22

33
import React, { useCallback, useEffect, useRef, useState } from 'react'
4-
import { ChevronDown } from 'lucide-react'
4+
import { ChevronDown } from '@/components/emcn/icons'
55
import { cn } from '@/lib/core/utils/cn'
6-
import type { ColumnDefinition, WorkflowGroup } from '@/lib/table'
6+
import type { WorkflowGroup } from '@/lib/table'
77
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
88
import { COL_WIDTH, SELECTION_TINT_BG } from '../constants'
99
import type { ColumnSourceInfo, DisplayColumn } from '../types'
@@ -21,7 +21,6 @@ interface ColumnHeaderMenuProps {
2121
onRenameSubmit: () => void
2222
onRenameCancel: () => void
2323
onColumnSelect: (colIndex: number, shiftKey: boolean) => void
24-
onChangeType: (columnName: string, newType: ColumnDefinition['type']) => void
2524
onInsertLeft: (columnName: string) => void
2625
onInsertRight: (columnName: string) => void
2726
onDeleteColumn: (columnName: string) => void
@@ -42,6 +41,14 @@ interface ColumnHeaderMenuProps {
4241
/** Opens a popup preview of the column's underlying workflow. Surfaced in
4342
* the chevron menu for workflow-output columns. */
4443
onViewWorkflow?: (workflowId: string) => void
44+
/** Whether this column is currently pinned to the left. */
45+
isPinned?: boolean
46+
/** Toggle the pinned state for this column. */
47+
onPinToggle?: (columnName: string) => void
48+
/** Left offset in pixels when pinned (drives `position: sticky`). */
49+
stickyLeft?: number
50+
/** Whether this is the rightmost pinned column (renders a separator shadow). */
51+
isLastPinned?: boolean
4552
}
4653

4754
/**
@@ -76,6 +83,10 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
7683
sourceInfo,
7784
onOpenConfig,
7885
onViewWorkflow,
86+
isPinned,
87+
onPinToggle,
88+
stickyLeft,
89+
isLastPinned,
7990
}: ColumnHeaderMenuProps) {
8091
const renameInputRef = useRef<HTMLInputElement>(null)
8192
const didDragRef = useRef(false)
@@ -228,7 +239,12 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
228239

229240
return (
230241
<th
231-
className='group relative border-[var(--border)] border-r border-b bg-[var(--bg)] p-0 text-left align-middle'
242+
className={cn(
243+
'group relative border-[var(--border)] border-r border-b bg-[var(--bg)] p-0 text-left align-middle',
244+
stickyLeft !== undefined && 'z-[11]',
245+
isLastPinned && '[box-shadow:2px_0_0_0_var(--border)]'
246+
)}
247+
style={stickyLeft !== undefined ? { position: 'sticky', left: stickyLeft } : undefined}
232248
draggable={!readOnly && !isRenaming}
233249
onDragStart={handleDragStart}
234250
onDragEnd={handleDragEnd}
@@ -316,6 +332,8 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
316332
onViewWorkflow={
317333
onViewWorkflow && ownGroup ? () => onViewWorkflow(ownGroup.workflowId) : undefined
318334
}
335+
isPinned={isPinned}
336+
onPinToggle={onPinToggle}
319337
/>
320338
</div>
321339
)}

0 commit comments

Comments
 (0)