@@ -15,6 +15,8 @@ import { attachSelectionContextToClipboard } from '@/lib/copilot/chat/selection-
1515import { captureEvent } from '@/lib/posthog/client'
1616import type {
1717 ColumnDefinition ,
18+ Predicate ,
19+ SortDirection ,
1820 TableLocks ,
1921 TableMetadata ,
2022 TablePredicate ,
@@ -24,6 +26,7 @@ import type {
2426import { getColumnId } from '@/lib/table/column-keys'
2527import { columnTypeOf } from '@/lib/table/column-types'
2628import { TABLE_LIMITS } from '@/lib/table/constants'
29+ import { cellValueFilterConditions } from '@/lib/table/query-builder/cell-filter'
2730import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
2831import type { RemoteTableSelection } from '@/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-room'
2932import type { BlockedTableAction } from '@/app/workspace/[workspaceId]/tables/[tableId]/lock-copy'
@@ -91,6 +94,7 @@ const logger = createLogger('TableView')
9194
9295const EMPTY_RUNNING_BY_ROW : Readonly < Record < string , number > > = Object . freeze ( { } )
9396const EMPTY_FIND_MATCHES : readonly TableFindMatch [ ] = Object . freeze ( [ ] )
97+ const EMPTY_FILTER_CONDITIONS : readonly Predicate [ ] = Object . freeze ( [ ] )
9498
9599const COL_WIDTH_MIN = 80
96100const COL_WIDTH_AUTO_FIT_MAX = 1000
@@ -238,6 +242,14 @@ interface TableGridProps {
238242 onSelectionChange : ( state : SelectionSnapshot ) => void
239243 /** Filter + sort. Lifted to wrapper so a single `useTable` call serves both. */
240244 queryOptions : QueryOptions
245+ /**
246+ * Narrows the active filter with the conditions matching one cell's value
247+ * ("Filter by cell value"). The wrapper owns the filter, so the grid only
248+ * reports the conditions the clicked cell produced.
249+ */
250+ onFilterByCellValue ?: ( conditions : readonly Predicate [ ] ) => void
251+ onSortColumn ?: ( columnId : string , direction : SortDirection ) => void
252+ onClearSort ?: ( ) => void
241253 /**
242254 * **Column ids** to hide from the grid. Owned by the wrapper because the filter
243255 * panel's Columns section edits the same list and the active view persists it.
@@ -438,6 +450,9 @@ export function TableGrid({
438450 onStopRow,
439451 onSelectionChange,
440452 queryOptions,
453+ onFilterByCellValue,
454+ onSortColumn,
455+ onClearSort,
441456 hiddenColumns,
442457 viewLayout,
443458 viewLayoutKey = null ,
@@ -556,6 +571,9 @@ export function TableGrid({
556571 filter : effectiveFilter ,
557572 } = useTable ( { workspaceId, tableId, queryOptions } )
558573
574+ /** Sort is single-column, so only the first spec entry can be active. */
575+ const activeSort = queryOptions . sort ?. [ 0 ]
576+
559577 const { data : tableRunState } = useTableRunState ( tableId )
560578 const activeDispatches = tableRunState ?. dispatches
561579 const runningByRowId = tableRunState ?. runningByRowId ?? EMPTY_RUNNING_BY_ROW
@@ -1203,23 +1221,43 @@ export function TableGrid({
12031221 [ ]
12041222 )
12051223
1224+ /** The right-clicked cell's column. One lookup shared by every menu item that
1225+ * needs it, rather than a scan per item. */
1226+ const contextMenuColumn = contextMenu . columnName
1227+ ? columnsRef . current . find ( ( c ) => getColumnId ( c ) === contextMenu . columnName )
1228+ : undefined
1229+
12061230 function handleContextMenuEditCell ( ) {
12071231 if ( contextMenu . row && contextMenu . columnName ) {
1208- const column = columnsRef . current . find ( ( c ) => getColumnId ( c ) === contextMenu . columnName )
1209- if ( column && columnTypeOf ( column ) . editor === 'toggle' ) {
1232+ if ( contextMenuColumn && columnTypeOf ( contextMenuColumn ) . editor === 'toggle' ) {
12101233 toggleBooleanCell (
12111234 contextMenu . row . id ,
12121235 contextMenu . columnName ,
12131236 contextMenu . row . data [ contextMenu . columnName ]
12141237 )
1215- } else if ( column ) {
1238+ } else if ( contextMenuColumn ) {
12161239 setEditingCell ( { rowId : contextMenu . row . id , columnName : contextMenu . columnName } )
12171240 setInitialCharacter ( null )
12181241 }
12191242 }
12201243 closeContextMenu ( )
12211244 }
12221245
1246+ /** Conditions matching the right-clicked cell; empty when it has none the
1247+ * filter grammar can express (see `cellValueFilterConditions`). Gated on
1248+ * `isOpen` because closing the menu leaves `row`/`columnName` set, and this
1249+ * would otherwise rebuild on every render of the grid for the rest of the
1250+ * session. */
1251+ const contextMenuFilterConditions =
1252+ contextMenu . isOpen && contextMenu . row && contextMenu . columnName
1253+ ? cellValueFilterConditions ( contextMenuColumn , contextMenu . row . data [ contextMenu . columnName ] )
1254+ : EMPTY_FILTER_CONDITIONS
1255+
1256+ function handleContextMenuFilterByCellValue ( ) {
1257+ onFilterByCellValue ?.( contextMenuFilterConditions )
1258+ closeContextMenu ( )
1259+ }
1260+
12231261 function handleContextMenuDelete ( ) {
12241262 const contextRow = contextMenu . row
12251263 if ( ! contextRow ) {
@@ -1301,7 +1339,7 @@ export function TableGrid({
13011339 // cascade re-runs dependents on its own) instead of every group on the row.
13021340 let contextMenuGroupId : string | null = null
13031341 if ( contextMenu . row && contextMenu . columnName ) {
1304- const _col = columnsRef . current . find ( ( c ) => getColumnId ( c ) === contextMenu . columnName )
1342+ const _col = contextMenuColumn
13051343 const _gid = _col ?. workflowGroupId
13061344 if ( _col && _gid ) {
13071345 const _exec = contextMenu . row . executions ?. [ _gid ]
@@ -4402,6 +4440,11 @@ export function TableGrid({
44024440 sourceInfo = { columnSourceInfo . get ( column . key ) }
44034441 onOpenConfig = { handleConfigureColumn }
44044442 onViewWorkflow = { handleViewWorkflow }
4443+ onSortColumn = { onSortColumn }
4444+ onClearSort = { onClearSort }
4445+ sortDirection = {
4446+ activeSort ?. field === column . key ? activeSort . direction : undefined
4447+ }
44054448 isPinned = { colIsPinned }
44064449 onPinToggle = { userPermissions . canEdit ? handlePinToggle : undefined }
44074450 stickyLeft = { colStickyLeft }
@@ -4574,6 +4617,11 @@ export function TableGrid({
45744617 Boolean ( contextMenuEnrichment )
45754618 }
45764619 canEditCell = { ! contextMenuIsWorkflowColumn }
4620+ onFilterByCellValue = {
4621+ onFilterByCellValue && contextMenuFilterConditions . length > 0
4622+ ? handleContextMenuFilterByCellValue
4623+ : undefined
4624+ }
45774625 selectedRowCount = { selectedRowCount }
45784626 onRunWorkflows = {
45794627 userPermissions . canEdit && hasWorkflowColumns && contextMenuStats . hasIncompleteOrFailed
0 commit comments