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,9 +1,13 @@
'use client'

import type { RowExecutionMetadata } from '@/lib/table'
import {
CellRender,
type ReferenceCellAction,
resolveCellRender,
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render'
import type { SaveReason } from '../../../types'
import type { DisplayColumn } from '../types'
import { CellRender, resolveCellRender } from './cell-render'
import { InlineEditor } from './inline-editors'

interface CellContentProps {
Expand All @@ -25,6 +29,8 @@ interface CellContentProps {
waitingOnLabels?: string[]
/** Column is an enrichment output — a completed-but-empty cell renders "Not found". */
isEnrichmentOutput?: boolean
/** Opens the inline row preview for a populated Reference cell. */
referenceAction?: ReferenceCellAction
}

/**
Expand All @@ -44,6 +50,7 @@ export function CellContent({
onCancel,
waitingOnLabels,
isEnrichmentOutput,
referenceAction,
}: CellContentProps) {
const kind = resolveCellRender({
value,
Expand All @@ -67,7 +74,7 @@ export function CellContent({
/>
</div>
)}
<CellRender kind={kind} isEditing={isEditing} />
<CellRender kind={kind} isEditing={isEditing} referenceAction={referenceAction} />
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* @vitest-environment jsdom
*/
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { DisplayColumn } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/types'

vi.mock('@sim/emcn', () => ({
Badge: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
Checkbox: () => null,
Chip: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button {...props}>{children}</button>
),
cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' '),
Tooltip: {
Root: ({ children }: { children: React.ReactNode }) => children,
Trigger: ({ children }: { children: React.ReactNode }) => children,
Content: ({ children }: { children: React.ReactNode }) => children,
},
}))

vi.mock('@/app/workspace/[workspaceId]/logs/utils', () => ({
StatusBadge: () => null,
}))

vi.mock(
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell',
() => ({ SimResourceCell: () => null })
)

vi.mock('@/app/workspace/[workspaceId]/tables/[tableId]/components/select-field', () => ({
resolveSelectOptions: () => [],
SelectPill: () => null,
}))

import {
CellRender,
resolveCellRender,
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render'

const REFERENCE_COLUMN: DisplayColumn = {
id: 'col-account',
key: 'col-account',
name: 'Account',
type: 'reference',
referenceTableId: 'table-accounts',
groupSize: 1,
groupStartColIndex: 0,
headerLabel: 'Account',
isGroupStart: true,
}

let container: HTMLDivElement
let root: Root

beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
container = document.createElement('div')
document.body.appendChild(container)
act(() => {
root = createRoot(container)
})
})

afterEach(() => {
act(() => root.unmount())
container.remove()
})

describe('reference cell rendering', () => {
it('resolves a stored row ID to a chip labeled with the reference column name', () => {
expect(
resolveCellRender({
value: 'row-account-1',
exec: undefined,
column: REFERENCE_COLUMN,
waitingOnLabels: undefined,
})
).toMatchObject({ kind: 'column-chip', label: 'Account' })
})

it('keeps an empty reference cell empty', () => {
expect(
resolveCellRender({
value: '',
exec: undefined,
column: REFERENCE_COLUMN,
waitingOnLabels: undefined,
})
).toEqual({ kind: 'empty' })
})

it('opens the referenced row from the chip without exposing its stored row ID', () => {
const onReferenceClick = vi.fn()

act(() => {
root.render(
<CellRender
kind={resolveCellRender({
value: 'row-account-1',
exec: undefined,
column: REFERENCE_COLUMN,
waitingOnLabels: undefined,
})}
isEditing={false}
referenceAction={{ expanded: false, onClick: onReferenceClick }}
/>
)
})

const chip = container.querySelector('button')
expect(chip?.textContent).toBe('Account')

act(() => chip?.click())

expect(onReferenceClick).toHaveBeenCalledOnce()
expect(container.textContent).not.toContain('row-account-1')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type React from 'react'
import { useEffect, useRef, useState } from 'react'
import { Badge, Checkbox, cn, Tooltip } from '@sim/emcn'
import { Badge, Checkbox, Chip, cn, Tooltip } from '@sim/emcn'
import { parse } from 'tldts'
import { faviconUrl } from '@/lib/core/utils/favicon'
import type { RowExecutionMetadata, SelectOption } from '@/lib/table'
Expand All @@ -28,6 +28,7 @@ export type CellRenderKind =
// Plain typed cells
| { kind: 'boolean'; checked: boolean }
| { kind: 'select'; options: SelectOption[] }
| { kind: 'column-chip'; label: string; icon: React.ComponentType<{ className?: string }> }
| { kind: 'json'; text: string }
| { kind: 'date'; text: string }
| { kind: 'url'; text: string; href: string; domain: string }
Expand Down Expand Up @@ -128,6 +129,17 @@ export function resolveCellRender({
if (column.type === 'select') {
return { kind: 'select', options: resolveSelectOptions(column, value) }
}
const typeDefinition = columnTypeOf(column)
if (typeDefinition.referencePreview) {
const rowId = typeDefinition.referencePreview.getRowId(value)
return rowId
? {
kind: 'column-chip',
label: typeDefinition.referencePreview.getChipLabel(column),
icon: typeDefinition.icon,
}
: { kind: 'empty' }
}
if (isNull) return { kind: 'empty' }
// Formatted here rather than in a render branch because the symbol and
// fraction digits come from the COLUMN's currency, which the render switch
Expand Down Expand Up @@ -251,9 +263,19 @@ function extractSimResourceInfo(
interface CellRenderProps {
kind: CellRenderKind
isEditing: boolean
referenceAction?: ReferenceCellAction
}

export interface ReferenceCellAction {
expanded: boolean
onClick: () => void
}

export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactElement | null {
export function CellRender({
kind,
isEditing,
referenceAction,
}: CellRenderProps): React.ReactElement | null {
const valueText = kind.kind === 'value' ? kind.text : null
const revealedValueText = useTypewriter(valueText)

Expand Down Expand Up @@ -375,6 +397,25 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
</span>
)

case 'column-chip': {
const ChipIcon = kind.icon
return (
<Chip
active={referenceAction?.expanded}
leftIcon={ChipIcon}
aria-expanded={referenceAction?.expanded}
disabled={!referenceAction}
className={cn('h-5 max-w-full', isEditing && 'invisible')}
onClick={(event) => {
event.stopPropagation()
referenceAction?.onClick()
}}
Comment on lines +409 to +412

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Double-clicking a reference chip still bubbles dblclick to the cell, so opening the preview can also enter inline edit mode. Stop double-click propagation on the chip, matching the URL cell behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx, line 409:

<comment>Double-clicking a reference chip still bubbles `dblclick` to the cell, so opening the preview can also enter inline edit mode. Stop double-click propagation on the chip, matching the URL cell behavior.</comment>

<file context>
@@ -375,6 +397,25 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
+          aria-expanded={referenceAction?.expanded}
+          disabled={!referenceAction}
+          className={cn('h-5 max-w-full', isEditing && 'invisible')}
+          onClick={(event) => {
+            event.stopPropagation()
+            referenceAction?.onClick()
</file context>
Suggested change
onClick={(event) => {
event.stopPropagation()
referenceAction?.onClick()
}}
onClick={(event) => {
event.stopPropagation()
referenceAction?.onClick()
}}
onDoubleClick={(event) => event.stopPropagation()}

>
{kind.label}
</Chip>
)
}

case 'json':
return (
<span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ import { Button, Checkbox, cn, handleKeyboardActivation } from '@sim/emcn'
import { PlayOutline, Square } from '@sim/emcn/icons'
import type { ActiveDispatch } from '@/lib/api/contracts/tables'
import type { TableRow as TableRowType, WorkflowGroup } from '@/lib/table'
import { columnTypeOf } from '@/lib/table/column-types'
import { getUnmetGroupDeps } from '@/lib/table/deps'
import type {
DisplayColumn,
ReferencePreviewTarget,
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/types'
import {
isSameReferencePreviewTarget,
type NormalizedSelection,
resolveCellExec,
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils'
import type { SaveReason } from '../../types'
import { CellContent } from './cells'
import {
Expand All @@ -17,8 +27,6 @@ import {
SELECTION_OVERLAY,
SELECTION_TINT_BG,
} from './constants'
import type { DisplayColumn } from './types'
import { type NormalizedSelection, resolveCellExec } from './utils'

export interface DataRowProps {
row: TableRowType
Expand Down Expand Up @@ -76,6 +84,8 @@ export interface DataRowProps {
* from re-running for a search elsewhere in the table.
*/
findMatchColumns?: ReadonlySet<string>
expandedReference: ReferencePreviewTarget | null
onReferenceClick: (target: ReferencePreviewTarget) => void
}

function cellRangeRowChanged(
Expand Down Expand Up @@ -138,7 +148,9 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
prev.activeDispatches !== next.activeDispatches ||
prev.pinnedOffsets !== next.pinnedOffsets ||
prev.lastPinnedColKey !== next.lastPinnedColKey ||
prev.findMatchColumns !== next.findMatchColumns
prev.findMatchColumns !== next.findMatchColumns ||
prev.expandedReference !== next.expandedReference ||
prev.onReferenceClick !== next.onReferenceClick
) {
return false
}
Expand Down Expand Up @@ -188,6 +200,8 @@ export const DataRow = React.memo(function DataRow({
pinnedOffsets,
lastPinnedColKey,
findMatchColumns,
expandedReference,
onReferenceClick,
}: DataRowProps) {
const sel = normalizedSelection
/**
Expand Down Expand Up @@ -301,6 +315,22 @@ export const DataRow = React.memo(function DataRow({
</div>
</td>
{columns.map((column, colIndex) => {
const value =
pendingCellValue && column.key in pendingCellValue
? pendingCellValue[column.key]
: row.data[column.key]
const referencePreview = columnTypeOf(column).referencePreview
const referenceRowId = referencePreview?.getRowId(value) ?? null
const referenceTableId = referencePreview?.getTableId(column)
const referenceTarget =
referenceTableId && referenceRowId
? {
sourceRowId: row.id,
sourceColumnKey: column.key,
referenceTableId,
referenceRowId,
}
: null
const inRange =
sel !== null &&
rowIndex >= sel.startRow &&
Expand Down Expand Up @@ -396,11 +426,7 @@ export const DataRow = React.memo(function DataRow({
<div className={CELL_CONTENT}>
<CellContent
workspaceId={workspaceId}
value={
pendingCellValue && column.key in pendingCellValue
? pendingCellValue[column.key]
: row.data[column.key]
}
value={value}
exec={resolveCellExec(
row,
column.workflowGroupId
Expand All @@ -424,6 +450,14 @@ export const DataRow = React.memo(function DataRow({
'enrichment'
: false
}
referenceAction={
referenceTarget
? {
expanded: isSameReferencePreviewTarget(expandedReference, referenceTarget),
onClick: () => onReferenceClick(referenceTarget),
}
: undefined
}
/>
</div>
</td>
Expand Down
Loading
Loading