11'use client'
22
33import type React from 'react'
4- import { useCallback , useRef , useState } from 'react'
4+ import { useRef , useState } from 'react'
55import {
66 DropdownMenu ,
77 DropdownMenuContent ,
@@ -17,9 +17,11 @@ import {
1717 ArrowRight ,
1818 Eye ,
1919 EyeOff ,
20+ Lock ,
2021 Pencil ,
2122 PlayOutline ,
2223 Trash ,
24+ Unlock ,
2325} from '@/components/emcn/icons'
2426import type { RunLimit , RunMode } from '@/lib/api/contracts/tables'
2527import { cn } from '@/lib/core/utils/cn'
@@ -67,6 +69,10 @@ interface ColumnOptionsMenuProps {
6769 /** When set, the menu surfaces a "View workflow" item that opens a popup
6870 * preview of the configured workflow. */
6971 onViewWorkflow ?: ( ) => void
72+ /** Whether this column is currently frozen (pinned to the left). */
73+ isFrozen ?: boolean
74+ /** Toggle the frozen state of this column. */
75+ onFreezeToggle ?: ( columnName : string ) => void
7076}
7177
7278/**
@@ -93,6 +99,8 @@ export function ColumnOptionsMenu({
9399 onRunColumnSelected,
94100 selectedRowCount = 0 ,
95101 onViewWorkflow,
102+ isFrozen,
103+ onFreezeToggle,
96104} : ColumnOptionsMenuProps ) {
97105 const showRunActions = Boolean ( onRunColumnAll && onRunColumnIncomplete )
98106 const showRunSelected = Boolean ( onRunColumnSelected ) && selectedRowCount > 0
@@ -159,6 +167,12 @@ export function ColumnOptionsMenu({
159167 < Pencil />
160168 Edit column
161169 </ DropdownMenuItem >
170+ { onFreezeToggle && (
171+ < DropdownMenuItem onSelect = { ( ) => onFreezeToggle ( column . name ) } >
172+ { isFrozen ? < Unlock /> : < Lock /> }
173+ { isFrozen ? 'Unfreeze column' : 'Freeze column' }
174+ </ DropdownMenuItem >
175+ ) }
162176 < DropdownMenuSeparator />
163177 < DropdownMenuItem onSelect = { ( ) => onInsertLeft ( column . name ) } >
164178 < ArrowLeft />
@@ -269,112 +283,94 @@ export function WorkflowGroupMetaCell({
269283
270284 const selectedCount = selectedRowIds ?. length ?? 0
271285
272- const handleRunAll = useCallback ( ( ) => {
286+ function handleRunAll ( ) {
273287 if ( groupId ) onRunColumn ?.( groupId , 'all' )
274- } , [ groupId , onRunColumn ] )
288+ }
275289
276- const handleRunIncomplete = useCallback ( ( ) => {
290+ function handleRunIncomplete ( ) {
277291 if ( groupId ) onRunColumn ?.( groupId , 'incomplete' )
278- } , [ groupId , onRunColumn ] )
292+ }
279293
280- const handleRunSelected = useCallback ( ( ) => {
294+ function handleRunSelected ( ) {
281295 if ( groupId && selectedRowIds && selectedRowIds . length > 0 ) {
282296 onRunColumn ?.( groupId , 'all' , selectedRowIds )
283297 }
284- } , [ groupId , onRunColumn , selectedRowIds ] )
298+ }
285299
286- const handleRunLimited = useCallback (
287- ( max : number ) => {
288- if ( groupId ) onRunColumn ?.( groupId , 'incomplete' , undefined , { type : 'rows' , max } )
289- } ,
290- [ groupId , onRunColumn ]
291- )
300+ function handleRunLimited ( max : number ) {
301+ if ( groupId ) onRunColumn ?.( groupId , 'incomplete' , undefined , { type : 'rows' , max } )
302+ }
292303
293- const handleContextMenu = useCallback (
294- ( e : React . MouseEvent ) => {
295- if ( ! column ) return
296- e . preventDefault ( )
297- e . stopPropagation ( )
298- setOptionsMenuPosition ( { x : e . clientX , y : e . clientY } )
299- setOptionsMenuOpen ( true )
300- } ,
301- [ column ]
302- )
304+ function handleContextMenu ( e : React . MouseEvent ) {
305+ if ( ! column ) return
306+ e . preventDefault ( )
307+ e . stopPropagation ( )
308+ setOptionsMenuPosition ( { x : e . clientX , y : e . clientY } )
309+ setOptionsMenuOpen ( true )
310+ }
303311
304- const selectGroupAndOpenConfig = useCallback (
305- ( e : React . MouseEvent < HTMLTableCellElement > ) => {
306- // Ignore clicks that landed on an interactive child (badge, play button,
307- // dropdown items rendered via portal). Only the bare meta-cell area
308- // should select the group + open the config sidebar.
309- const target = e . target as HTMLElement
310- if ( target . closest ( 'button, [role="menuitem"], [role="menu"]' ) ) return
311- // Drag-vs-click guard: when a drag just ended on this cell, swallow the
312- // synthetic click so we don't accidentally pop open the sidebar.
313- if ( didDragRef . current ) {
314- didDragRef . current = false
315- return
316- }
317- onSelectGroup ( startColIndex , size )
318- if ( columnName ) onOpenConfig ( columnName )
319- } ,
320- [ columnName , onOpenConfig , onSelectGroup , size , startColIndex ]
321- )
312+ function selectGroupAndOpenConfig ( e : React . MouseEvent < HTMLTableCellElement > ) {
313+ // Ignore clicks that landed on an interactive child (badge, play button,
314+ // dropdown items rendered via portal). Only the bare meta-cell area
315+ // should select the group + open the config sidebar.
316+ const target = e . target as HTMLElement
317+ if ( target . closest ( 'button, [role="menuitem"], [role="menu"]' ) ) return
318+ // Drag-vs-click guard: when a drag just ended on this cell, swallow the
319+ // synthetic click so we don't accidentally pop open the sidebar.
320+ if ( didDragRef . current ) {
321+ didDragRef . current = false
322+ return
323+ }
324+ onSelectGroup ( startColIndex , size )
325+ if ( columnName ) onOpenConfig ( columnName )
326+ }
322327
323- const handleDragStart = useCallback (
324- ( e : React . DragEvent ) => {
325- if ( readOnly || ! onDragStart || ! columnName ) {
326- e . preventDefault ( )
327- return
328- }
329- didDragRef . current = true
330- e . dataTransfer . effectAllowed = 'move'
331- e . dataTransfer . setData ( 'text/plain' , columnName )
328+ function handleDragStart ( e : React . DragEvent ) {
329+ if ( readOnly || ! onDragStart || ! columnName ) {
330+ e . preventDefault ( )
331+ return
332+ }
333+ didDragRef . current = true
334+ e . dataTransfer . effectAllowed = 'move'
335+ e . dataTransfer . setData ( 'text/plain' , columnName )
332336
333- const ghost = document . createElement ( 'div' )
334- ghost . textContent = name
335- ghost . style . cssText =
336- 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:13px;font-weight:500;white-space:nowrap;color:var(--text-primary)'
337- document . body . appendChild ( ghost )
338- e . dataTransfer . setDragImage ( ghost , ghost . offsetWidth / 2 , ghost . offsetHeight / 2 )
339- requestAnimationFrame ( ( ) => ghost . parentNode ?. removeChild ( ghost ) )
337+ const ghost = document . createElement ( 'div' )
338+ ghost . textContent = name
339+ ghost . style . cssText =
340+ 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:13px;font-weight:500;white-space:nowrap;color:var(--text-primary)'
341+ document . body . appendChild ( ghost )
342+ e . dataTransfer . setDragImage ( ghost , ghost . offsetWidth / 2 , ghost . offsetHeight / 2 )
343+ requestAnimationFrame ( ( ) => ghost . parentNode ?. removeChild ( ghost ) )
340344
341- onDragStart ( columnName )
342- } ,
343- [ columnName , name , onDragStart , readOnly ]
344- )
345+ onDragStart ( columnName )
346+ }
345347
346- const handleDragOver = useCallback (
347- ( e : React . DragEvent ) => {
348- if ( ! onDragOver || ! columnName ) return
349- e . preventDefault ( )
350- e . dataTransfer . dropEffect = 'move'
351- const rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( )
352- const midX = rect . left + rect . width / 2
353- const side = e . clientX < midX ? 'left' : 'right'
354- onDragOver ( columnName , side )
355- } ,
356- [ columnName , onDragOver ]
357- )
348+ function handleDragOver ( e : React . DragEvent ) {
349+ if ( ! onDragOver || ! columnName ) return
350+ e . preventDefault ( )
351+ e . dataTransfer . dropEffect = 'move'
352+ const rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( )
353+ const midX = rect . left + rect . width / 2
354+ const side = e . clientX < midX ? 'left' : 'right'
355+ onDragOver ( columnName , side )
356+ }
358357
359- const handleDragEnd = useCallback ( ( ) => {
358+ function handleDragEnd ( ) {
360359 didDragRef . current = false
361360 onDragEnd ?.( )
362- } , [ onDragEnd ] )
361+ }
363362
364- const handleDragLeave = useCallback (
365- ( e : React . DragEvent ) => {
366- const th = e . currentTarget as HTMLElement
367- const related = e . relatedTarget as Node | null
368- if ( related && th . contains ( related ) ) return
369- if ( related && related instanceof Element && related . closest ( 'th' ) ) return
370- onDragLeave ?.( )
371- } ,
372- [ onDragLeave ]
373- )
363+ function handleDragLeave ( e : React . DragEvent ) {
364+ const th = e . currentTarget as HTMLElement
365+ const related = e . relatedTarget as Node | null
366+ if ( related && th . contains ( related ) ) return
367+ if ( related && related instanceof Element && related . closest ( 'th' ) ) return
368+ onDragLeave ?.( )
369+ }
374370
375- const handleDrop = useCallback ( ( e : React . DragEvent ) => {
371+ function handleDrop ( e : React . DragEvent ) {
376372 e . preventDefault ( )
377- } , [ ] )
373+ }
378374
379375 const isDraggable = ! readOnly && Boolean ( onDragStart )
380376
0 commit comments