Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type {MouseEvent} from "react"

import {message} from "@agenta/ui/app-message"
import {getDefaultStore} from "jotai"
import Router from "next/router"
Expand All @@ -23,14 +21,6 @@ const getUrlState = (): URLState => store.get(urlAtom) as URLState

const getActiveAppId = (): string | null => store.get(routerAppIdAtom)

export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement | null
if (!target) return false
const interactiveSelector =
"button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, .ant-btn, .ant-select, .ant-dropdown-trigger"
return Boolean(target.closest(interactiveSelector))
}

interface NavigateToRunParams {
record: EvaluationRunTableRow
scope: "app" | "project"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {Key, MouseEvent, ReactNode} from "react"
import type {Key, ReactNode} from "react"
import {useCallback, useEffect, useMemo, useRef, useState} from "react"

import {useQueryClient} from "@tanstack/react-query"
Expand Down Expand Up @@ -34,7 +34,6 @@ import {
} from "@/oss/lib/onboarding"
import {useQueryParamState} from "@/oss/state/appState"

import {shouldIgnoreRowClick} from "../../actions/navigationActions"
import {
evaluationRunsDeleteContextAtom,
evaluationRunsTableFetchEnabledAtom,
Expand Down Expand Up @@ -333,8 +332,7 @@ const EvaluationRunsTableActive = ({
const runId = record.preview?.id ?? record.runId
const isNavigable = Boolean(!record.__isSkeleton && runId)
return {
onClick: (event: MouseEvent<HTMLTableRowElement>) => {
if (shouldIgnoreRowClick(event)) return
onClick: () => {
if (!isNavigable) return
handleOpenRun(record)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ function createActionsColumn<T extends InfiniteTableRowBase>(
align: "center",
// Lock actions column from being toggled in visibility menu
columnVisibilityLocked: true as any,
onCell: () => ({className: "ag-table-actions-cell"}),
render: (_, record) => {
if (record.__isSkeleton) return null

Expand Down Expand Up @@ -294,7 +295,10 @@ function createActionsColumn<T extends InfiniteTableRowBase>(
}

return (
<div className="h-full flex items-center justify-center">
<div
className="w-full h-full flex items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
<Dropdown
trigger={["click"]}
styles={{root: {width: 200}}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {ReactNode} from "react"
import type {MouseEvent, ReactNode} from "react"

import type {ColumnsType} from "antd/es/table"
import clsx from "clsx"
Expand Down Expand Up @@ -132,6 +132,24 @@ const buildColumn = <Row extends object>(
;(column as any).exportMetadata = config.exportMetadata
}

// Auto-stop click propagation in action columns so clicks on empty cell area
// don't bubble to the row navigation handler.
if (config.key === "actions") {
const prevOnCell = column.onCell as ((record: Row, index?: number) => any) | undefined
column.onCell = (record: Row, index?: number) => {
const base = prevOnCell ? prevOnCell(record, index) : {}
const prevClick = (base as any)?.onClick
return {
...base,
className: clsx((base as any)?.className, "ag-table-actions-cell"),
onClick: (e: MouseEvent<HTMLTableDataCellElement>) => {
e.stopPropagation()
prevClick?.(e)
},
}
}
}

return column
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import useInfiniteScroll from "../hooks/useInfiniteScroll"
import useScrollContainer from "../hooks/useScrollContainer"
import useSmartResizableColumns from "../hooks/useSmartResizableColumns"
import useTableKeyboardShortcuts from "../hooks/useTableKeyboardShortcuts"
import {shouldIgnoreRowClick} from "../hooks/useTableManager"
import useTableRowSelection from "../hooks/useTableRowSelection"
import ColumnVisibilityProvider from "../providers/ColumnVisibilityProvider"
import type {InfiniteVirtualTableProps} from "../types"
Expand Down Expand Up @@ -69,6 +70,7 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
keyboardShortcuts,
expandable,
tableRef,
disableInteractiveClickGuard = false,
}: InfiniteVirtualTableInnerProps<RecordType>) => {
const generatedScopeId = useId()
const resolvedScopeId = useMemo(
Expand Down Expand Up @@ -500,28 +502,43 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
const shortcutProps = getShortcutRowProps
? (getShortcutRowProps(record, index) ?? {})
: {}
if (!shortcutProps || Object.keys(shortcutProps).length === 0) {
return baseProps

const baseOnClick = baseProps?.onClick
const guardedOnClick =
!disableInteractiveClickGuard && baseOnClick
? (event: React.MouseEvent<HTMLTableRowElement>) => {
if (shouldIgnoreRowClick(event)) return
baseOnClick(event)
}
: baseOnClick

const hasShortcuts = shortcutProps && Object.keys(shortcutProps).length > 0
if (!hasShortcuts) {
if (guardedOnClick === baseOnClick) return baseProps
return {...baseProps, onClick: guardedOnClick}
}
return {
...baseProps,
...shortcutProps,
className: clsx(baseProps?.className, shortcutProps?.className),
onMouseEnter: mergeHandlers(baseProps?.onMouseEnter, shortcutProps?.onMouseEnter),
onClick: guardedOnClick,
}
},
[finalTableProps.onRow, getShortcutRowProps],
[finalTableProps.onRow, getShortcutRowProps, disableInteractiveClickGuard],
)

const tablePropsWithShortcuts = useMemo<TableProps<RecordType>>(() => {
if (!getShortcutRowProps) {
const needsMerge =
getShortcutRowProps || (Boolean(finalTableProps.onRow) && !disableInteractiveClickGuard)
if (!needsMerge) {
return finalTableProps
}
return {
...finalTableProps,
onRow: mergedOnRow,
}
}, [finalTableProps, getShortcutRowProps, mergedOnRow])
}, [finalTableProps, getShortcutRowProps, mergedOnRow, disableInteractiveClickGuard])

const tableRowSelection = useTableRowSelection(rowSelection)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,18 @@ import useTableExport from "./useTableExport"
/** Stable no-op atom used when no external search atom is provided (hooks can't be conditional) */
const dummySearchAtom = atom("")

const INTERACTIVE_SELECTOR =
"button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], " +
".ant-btn, .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, " +
".ant-select, .ant-dropdown-trigger, .ant-table-selection-column, .ag-table-actions-cell"

/**
* Helper to detect if a click event should be ignored for row navigation
* Returns true if the click was on an interactive element (button, link, dropdown, etc.)
* Returns true when the click originated from an interactive element (button, link,
* dropdown, checkbox, etc.) and should not bubble up to the row navigation handler.
*/
export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>): boolean => {
const target = event.target as HTMLElement

// Check if clicking on interactive elements
if (
target.closest("button") ||
target.closest("a") ||
target.closest(".ant-dropdown-trigger") ||
target.closest(".ant-checkbox-wrapper") ||
target.closest(".ant-select") ||
target.closest("input") ||
target.closest("textarea")
) {
return true
}

return false
const target = event.target as HTMLElement | null
return Boolean(target?.closest(INTERACTIVE_SELECTOR))
}

/** Configuration for built-in search. When provided, the hook manages search state internally. */
Expand Down
6 changes: 6 additions & 0 deletions web/oss/src/components/InfiniteVirtualTable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,12 @@ export interface InfiniteVirtualTableProps<RecordType, ExpandedChildType = unkno
rowSelection?: InfiniteVirtualTableRowSelection<RecordType>
resizableColumns?: boolean | ResizableColumnsConfig
columnVisibility?: ColumnVisibilityConfig<RecordType>
/**
* When true, disables the built-in guard that prevents row-click navigation
* from firing when the click originates from an interactive element (button,
* checkbox, dropdown, etc.). Defaults to false — the guard is on by default.
*/
disableInteractiveClickGuard?: boolean
onColumnToggle?: (payload: {
scopeId: string | null
columnKey: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const TestcaseSelectionCell = memo(function TestcaseSelectionCell({
className="flex items-center justify-center w-full h-full absolute inset-0"
title={tooltipTitle}
style={showDirtyIndicator ? {backgroundColor: "rgb(255 251 235)"} : undefined}
onClick={(e) => e.stopPropagation()}
>
<div className="relative">{originNode}</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,21 +696,27 @@ export function TestcasesTableShell(props: TestcasesTableShellProps) {
align: "center",
columnVisibilityLocked: true as any,
exportEnabled: false as any, // Exclude from client-side CSV export
onCell: () => ({className: "ag-table-actions-cell"}),
render: (_, record) => {
if (record.__isSkeleton || isShowingSkeleton) return null

return (
<TestcaseRowActionsDropdown
testcaseId={record.id ? String(record.id) : String(record.key)}
onEdit={() => {
if (record.id) onRowClick(record)
}}
onDelete={() => {
if (record.key) {
table.deleteTestcases([String(record.key)])
}
}}
/>
<div
className="w-full h-full flex items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
<TestcaseRowActionsDropdown
testcaseId={record.id ? String(record.id) : String(record.key)}
onEdit={() => {
if (record.id) onRowClick(record)
}}
onDelete={() => {
if (record.key) {
table.deleteTestcases([String(record.key)])
}
}}
/>
</div>
)
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ function createActionsColumn<T extends InfiniteTableRowBase>(
columnVisibilityLocked: true,
// Exclude actions column from CSV export
exportEnabled: false,
onCell: () => ({className: "ag-table-actions-cell"}),
render: (_, record) => {
if (record.__isSkeleton) return null

Expand Down Expand Up @@ -342,7 +343,10 @@ function createActionsColumn<T extends InfiniteTableRowBase>(
}

return (
<div className="h-full flex items-center justify-center">
<div
className="w-full h-full flex items-center justify-center"
onClick={(e) => e.stopPropagation()}
>
<Dropdown
trigger={["click"]}
styles={{root: {width: 200}}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import useInfiniteScroll from "../hooks/useInfiniteScroll"
import useScrollContainer from "../hooks/useScrollContainer"
import useSmartResizableColumns from "../hooks/useSmartResizableColumns"
import useTableKeyboardShortcuts from "../hooks/useTableKeyboardShortcuts"
import {shouldIgnoreRowClick} from "../hooks/useTableManager"
import useTableRowSelection from "../hooks/useTableRowSelection"
import {useTypeChipColumns} from "../hooks/useTypeChipColumns"
import {useTypeChipFeature} from "../hooks/useTypeChipFeature"
Expand Down Expand Up @@ -74,6 +75,7 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
expandable,
tableRef,
typeChips,
disableInteractiveClickGuard = false,
}: InfiniteVirtualTableInnerProps<RecordType>) => {
const generatedScopeId = useId()
const resolvedScopeId = useMemo(
Expand Down Expand Up @@ -612,7 +614,16 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
}
: {}

const allProps = {
const rawOnClick = mergeHandlers(baseProps?.onClick, selectionProps?.onClick)
const onClick =
!disableInteractiveClickGuard && rawOnClick
? (event: MouseEvent<HTMLElement>) => {
if (shouldIgnoreRowClick(event)) return
rawOnClick(event as MouseEvent<HTMLTableRowElement>)
}
: rawOnClick

return {
...baseProps,
...shortcutProps,
...selectionProps,
Expand All @@ -622,23 +633,37 @@ const InfiniteVirtualTableInnerBase = <RecordType extends object>({
selectionProps?.className,
),
onMouseEnter: mergeHandlers(baseProps?.onMouseEnter, shortcutProps?.onMouseEnter),
onClick: mergeHandlers(baseProps?.onClick, selectionProps?.onClick),
onClick,
}

return allProps
},
[finalTableProps.onRow, getShortcutRowProps, selectOnRowClick, handleSelectionRowClick],
[
finalTableProps.onRow,
getShortcutRowProps,
selectOnRowClick,
handleSelectionRowClick,
disableInteractiveClickGuard,
],
)

const tablePropsWithShortcuts = useMemo<TableProps<RecordType>>(() => {
if (!getShortcutRowProps && !selectOnRowClick) {
const needsMerge =
getShortcutRowProps ||
selectOnRowClick ||
(Boolean(finalTableProps.onRow) && !disableInteractiveClickGuard)
if (!needsMerge) {
return finalTableProps
}
return {
...finalTableProps,
onRow: mergedOnRow,
}
}, [finalTableProps, getShortcutRowProps, selectOnRowClick, mergedOnRow])
}, [
finalTableProps,
getShortcutRowProps,
selectOnRowClick,
mergedOnRow,
disableInteractiveClickGuard,
])

const tableRowSelection = useTableRowSelection(rowSelection)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,18 @@ import useTableExport from "./useTableExport"
/** Stable no-op atom used when no external search atom is provided (hooks can't be conditional) */
const dummySearchAtom = atom("")

const INTERACTIVE_SELECTOR =
"button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], " +
".ant-btn, .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, " +
".ant-select, .ant-dropdown-trigger, .ant-table-selection-column, .ag-table-actions-cell"

/**
* Helper to detect if a click event should be ignored for row navigation
* Returns true if the click was on an interactive element (button, link, dropdown, etc.)
* Returns true when the click originated from an interactive element (button, link,
* dropdown, checkbox, etc.) and should not bubble up to the row navigation handler.
*/
export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>): boolean => {
const target = event.target as HTMLElement

// Check if clicking on interactive elements
if (
target.closest("button") ||
target.closest("a") ||
target.closest(".ant-dropdown-trigger") ||
target.closest(".ant-checkbox-wrapper") ||
target.closest(".ant-select") ||
target.closest("input") ||
target.closest("textarea")
) {
return true
}

return false
const target = event.target as HTMLElement | null
return Boolean(target?.closest(INTERACTIVE_SELECTOR))
}

/** Configuration for built-in search. When provided, the hook manages search state internally. */
Expand Down
Loading
Loading