Skip to content

Commit 6e2040a

Browse files
committed
feat(tables): preview referenced rows inline
1 parent c7f0c4d commit 6e2040a

16 files changed

Lines changed: 819 additions & 73 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx

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

33
import type { RowExecutionMetadata } from '@/lib/table'
4+
import {
5+
CellRender,
6+
type ReferenceCellAction,
7+
resolveCellRender,
8+
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render'
49
import type { SaveReason } from '../../../types'
510
import type { DisplayColumn } from '../types'
6-
import { CellRender, resolveCellRender } from './cell-render'
711
import { InlineEditor } from './inline-editors'
812

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

3036
/**
@@ -44,6 +50,7 @@ export function CellContent({
4450
onCancel,
4551
waitingOnLabels,
4652
isEnrichmentOutput,
53+
referenceAction,
4754
}: CellContentProps) {
4855
const kind = resolveCellRender({
4956
value,
@@ -67,7 +74,7 @@ export function CellContent({
6774
/>
6875
</div>
6976
)}
70-
<CellRender kind={kind} isEditing={isEditing} />
77+
<CellRender kind={kind} isEditing={isEditing} referenceAction={referenceAction} />
7178
</>
7279
)
7380
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
import type { DisplayColumn } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/types'
8+
9+
vi.mock('@sim/emcn', () => ({
10+
Badge: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
11+
Checkbox: () => null,
12+
Chip: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
13+
<button {...props}>{children}</button>
14+
),
15+
cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' '),
16+
Tooltip: {
17+
Root: ({ children }: { children: React.ReactNode }) => children,
18+
Trigger: ({ children }: { children: React.ReactNode }) => children,
19+
Content: ({ children }: { children: React.ReactNode }) => children,
20+
},
21+
}))
22+
23+
vi.mock('@/app/workspace/[workspaceId]/logs/utils', () => ({
24+
StatusBadge: () => null,
25+
}))
26+
27+
vi.mock(
28+
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell',
29+
() => ({ SimResourceCell: () => null })
30+
)
31+
32+
vi.mock('@/app/workspace/[workspaceId]/tables/[tableId]/components/select-field', () => ({
33+
resolveSelectOptions: () => [],
34+
SelectPill: () => null,
35+
}))
36+
37+
import {
38+
CellRender,
39+
resolveCellRender,
40+
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render'
41+
42+
const REFERENCE_COLUMN: DisplayColumn = {
43+
id: 'col-account',
44+
key: 'col-account',
45+
name: 'Account',
46+
type: 'reference',
47+
referenceTableId: 'table-accounts',
48+
groupSize: 1,
49+
groupStartColIndex: 0,
50+
headerLabel: 'Account',
51+
isGroupStart: true,
52+
}
53+
54+
let container: HTMLDivElement
55+
let root: Root
56+
57+
beforeEach(() => {
58+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
59+
container = document.createElement('div')
60+
document.body.appendChild(container)
61+
act(() => {
62+
root = createRoot(container)
63+
})
64+
})
65+
66+
afterEach(() => {
67+
act(() => root.unmount())
68+
container.remove()
69+
})
70+
71+
describe('reference cell rendering', () => {
72+
it('resolves a stored row ID to a chip labeled with the reference column name', () => {
73+
expect(
74+
resolveCellRender({
75+
value: 'row-account-1',
76+
exec: undefined,
77+
column: REFERENCE_COLUMN,
78+
waitingOnLabels: undefined,
79+
})
80+
).toMatchObject({ kind: 'column-chip', label: 'Account' })
81+
})
82+
83+
it('keeps an empty reference cell empty', () => {
84+
expect(
85+
resolveCellRender({
86+
value: '',
87+
exec: undefined,
88+
column: REFERENCE_COLUMN,
89+
waitingOnLabels: undefined,
90+
})
91+
).toEqual({ kind: 'empty' })
92+
})
93+
94+
it('opens the referenced row from the chip without exposing its stored row ID', () => {
95+
const onReferenceClick = vi.fn()
96+
97+
act(() => {
98+
root.render(
99+
<CellRender
100+
kind={resolveCellRender({
101+
value: 'row-account-1',
102+
exec: undefined,
103+
column: REFERENCE_COLUMN,
104+
waitingOnLabels: undefined,
105+
})}
106+
isEditing={false}
107+
referenceAction={{ expanded: false, onClick: onReferenceClick }}
108+
/>
109+
)
110+
})
111+
112+
const chip = container.querySelector('button')
113+
expect(chip?.textContent).toBe('Account')
114+
115+
act(() => chip?.click())
116+
117+
expect(onReferenceClick).toHaveBeenCalledOnce()
118+
expect(container.textContent).not.toContain('row-account-1')
119+
})
120+
})

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import type React from 'react'
44
import { useEffect, useRef, useState } from 'react'
5-
import { Badge, Checkbox, cn, Tooltip } from '@sim/emcn'
5+
import { Badge, Checkbox, Chip, cn, Tooltip } from '@sim/emcn'
66
import { parse } from 'tldts'
77
import { faviconUrl } from '@/lib/core/utils/favicon'
88
import type { RowExecutionMetadata, SelectOption } from '@/lib/table'
@@ -28,6 +28,7 @@ export type CellRenderKind =
2828
// Plain typed cells
2929
| { kind: 'boolean'; checked: boolean }
3030
| { kind: 'select'; options: SelectOption[] }
31+
| { kind: 'column-chip'; label: string; icon: React.ComponentType<{ className?: string }> }
3132
| { kind: 'json'; text: string }
3233
| { kind: 'date'; text: string }
3334
| { kind: 'url'; text: string; href: string; domain: string }
@@ -128,6 +129,17 @@ export function resolveCellRender({
128129
if (column.type === 'select') {
129130
return { kind: 'select', options: resolveSelectOptions(column, value) }
130131
}
132+
const typeDefinition = columnTypeOf(column)
133+
if (typeDefinition.referencePreview) {
134+
const rowId = typeDefinition.referencePreview.getRowId(value)
135+
return rowId
136+
? {
137+
kind: 'column-chip',
138+
label: typeDefinition.referencePreview.getChipLabel(column),
139+
icon: typeDefinition.icon,
140+
}
141+
: { kind: 'empty' }
142+
}
131143
if (isNull) return { kind: 'empty' }
132144
// Formatted here rather than in a render branch because the symbol and
133145
// fraction digits come from the COLUMN's currency, which the render switch
@@ -251,9 +263,19 @@ function extractSimResourceInfo(
251263
interface CellRenderProps {
252264
kind: CellRenderKind
253265
isEditing: boolean
266+
referenceAction?: ReferenceCellAction
267+
}
268+
269+
export interface ReferenceCellAction {
270+
expanded: boolean
271+
onClick: () => void
254272
}
255273

256-
export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactElement | null {
274+
export function CellRender({
275+
kind,
276+
isEditing,
277+
referenceAction,
278+
}: CellRenderProps): React.ReactElement | null {
257279
const valueText = kind.kind === 'value' ? kind.text : null
258280
const revealedValueText = useTypewriter(valueText)
259281

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

400+
case 'column-chip': {
401+
const ChipIcon = kind.icon
402+
return (
403+
<Chip
404+
active={referenceAction?.expanded}
405+
leftIcon={ChipIcon}
406+
aria-expanded={referenceAction?.expanded}
407+
disabled={!referenceAction}
408+
className={cn('h-5 max-w-full', isEditing && 'invisible')}
409+
onClick={(event) => {
410+
event.stopPropagation()
411+
referenceAction?.onClick()
412+
}}
413+
>
414+
{kind.label}
415+
</Chip>
416+
)
417+
}
418+
378419
case 'json':
379420
return (
380421
<span

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

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ import { Button, Checkbox, cn, handleKeyboardActivation } from '@sim/emcn'
55
import { PlayOutline, Square } from '@sim/emcn/icons'
66
import type { ActiveDispatch } from '@/lib/api/contracts/tables'
77
import type { TableRow as TableRowType, WorkflowGroup } from '@/lib/table'
8+
import { columnTypeOf } from '@/lib/table/column-types'
89
import { getUnmetGroupDeps } from '@/lib/table/deps'
10+
import type {
11+
DisplayColumn,
12+
ReferencePreviewTarget,
13+
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/types'
14+
import {
15+
isSameReferencePreviewTarget,
16+
type NormalizedSelection,
17+
resolveCellExec,
18+
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils'
919
import type { SaveReason } from '../../types'
1020
import { CellContent } from './cells'
1121
import {
@@ -17,8 +27,6 @@ import {
1727
SELECTION_OVERLAY,
1828
SELECTION_TINT_BG,
1929
} from './constants'
20-
import type { DisplayColumn } from './types'
21-
import { type NormalizedSelection, resolveCellExec } from './utils'
2230

2331
export interface DataRowProps {
2432
row: TableRowType
@@ -76,6 +84,8 @@ export interface DataRowProps {
7684
* from re-running for a search elsewhere in the table.
7785
*/
7886
findMatchColumns?: ReadonlySet<string>
87+
expandedReference: ReferencePreviewTarget | null
88+
onReferenceClick: (target: ReferencePreviewTarget) => void
7989
}
8090

8191
function cellRangeRowChanged(
@@ -138,7 +148,9 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
138148
prev.activeDispatches !== next.activeDispatches ||
139149
prev.pinnedOffsets !== next.pinnedOffsets ||
140150
prev.lastPinnedColKey !== next.lastPinnedColKey ||
141-
prev.findMatchColumns !== next.findMatchColumns
151+
prev.findMatchColumns !== next.findMatchColumns ||
152+
prev.expandedReference !== next.expandedReference ||
153+
prev.onReferenceClick !== next.onReferenceClick
142154
) {
143155
return false
144156
}
@@ -188,6 +200,8 @@ export const DataRow = React.memo(function DataRow({
188200
pinnedOffsets,
189201
lastPinnedColKey,
190202
findMatchColumns,
203+
expandedReference,
204+
onReferenceClick,
191205
}: DataRowProps) {
192206
const sel = normalizedSelection
193207
/**
@@ -301,6 +315,22 @@ export const DataRow = React.memo(function DataRow({
301315
</div>
302316
</td>
303317
{columns.map((column, colIndex) => {
318+
const value =
319+
pendingCellValue && column.key in pendingCellValue
320+
? pendingCellValue[column.key]
321+
: row.data[column.key]
322+
const referencePreview = columnTypeOf(column).referencePreview
323+
const referenceRowId = referencePreview?.getRowId(value) ?? null
324+
const referenceTableId = referencePreview?.getTableId(column)
325+
const referenceTarget =
326+
referenceTableId && referenceRowId
327+
? {
328+
sourceRowId: row.id,
329+
sourceColumnKey: column.key,
330+
referenceTableId,
331+
referenceRowId,
332+
}
333+
: null
304334
const inRange =
305335
sel !== null &&
306336
rowIndex >= sel.startRow &&
@@ -396,11 +426,7 @@ export const DataRow = React.memo(function DataRow({
396426
<div className={CELL_CONTENT}>
397427
<CellContent
398428
workspaceId={workspaceId}
399-
value={
400-
pendingCellValue && column.key in pendingCellValue
401-
? pendingCellValue[column.key]
402-
: row.data[column.key]
403-
}
429+
value={value}
404430
exec={resolveCellExec(
405431
row,
406432
column.workflowGroupId
@@ -424,6 +450,14 @@ export const DataRow = React.memo(function DataRow({
424450
'enrichment'
425451
: false
426452
}
453+
referenceAction={
454+
referenceTarget
455+
? {
456+
expanded: isSameReferencePreviewTarget(expandedReference, referenceTarget),
457+
onClick: () => onReferenceClick(referenceTarget),
458+
}
459+
: undefined
460+
}
427461
/>
428462
</div>
429463
</td>

0 commit comments

Comments
 (0)