Skip to content

Commit 073495c

Browse files
committed
feat(tables): preview referenced rows inline
1 parent c6f653a commit 073495c

34 files changed

Lines changed: 3181 additions & 180 deletions

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
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 { TimezoneState } from '@/hooks/queries/general-settings'
510
import type { SaveReason } from '../../../types'
611
import type { DisplayColumn } from '../types'
7-
import { CellRender, resolveCellRender } from './cell-render'
812
import { InlineEditor } from './inline-editors'
913

1014
interface CellContentProps {
@@ -16,6 +20,7 @@ interface CellContentProps {
1620
workspaceId: string
1721
timeZone: string
1822
timezoneStatus: TimezoneState['status']
23+
referenceColumnsEnabled: boolean
1924
isEditing: boolean
2025
initialCharacter?: string | null
2126
onSave: (value: unknown, reason: SaveReason) => void
@@ -28,6 +33,7 @@ interface CellContentProps {
2833
waitingOnLabels?: string[]
2934
/** Column is an enrichment output — a completed-but-empty cell renders "Not found". */
3035
isEnrichmentOutput?: boolean
36+
referenceAction?: ReferenceCellAction
3137
}
3238

3339
/**
@@ -43,12 +49,14 @@ export function CellContent({
4349
workspaceId,
4450
timeZone,
4551
timezoneStatus,
52+
referenceColumnsEnabled,
4653
isEditing,
4754
initialCharacter,
4855
onSave,
4956
onCancel,
5057
waitingOnLabels,
5158
isEnrichmentOutput,
59+
referenceAction,
5260
}: CellContentProps) {
5361
const kind = resolveCellRender({
5462
value,
@@ -59,6 +67,7 @@ export function CellContent({
5967
currentWorkspaceId: workspaceId,
6068
timeZone,
6169
timezoneStatus,
70+
referenceColumnsEnabled,
6271
})
6372

6473
return (
@@ -74,7 +83,7 @@ export function CellContent({
7483
/>
7584
</div>
7685
)}
77-
<CellRender kind={kind} isEditing={isEditing} />
86+
<CellRender kind={kind} isEditing={isEditing} referenceAction={referenceAction} />
7887
</>
7988
)
8089
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('resolveCellRender', () => {
3030
exec: undefined,
3131
column: column('ttl'),
3232
waitingOnLabels: undefined,
33+
referenceColumnsEnabled: false,
3334
timeZone: 'America/New_York',
3435
})
3536
).toEqual({ kind: 'date', text: '2023-11-14T17:13:20-05:00' })
@@ -42,6 +43,7 @@ describe('resolveCellRender', () => {
4243
exec: undefined,
4344
column: column('ttl'),
4445
waitingOnLabels: undefined,
46+
referenceColumnsEnabled: false,
4547
timeZone: 'America/Los_Angeles',
4648
timezoneStatus: 'invalid',
4749
})
@@ -55,6 +57,7 @@ describe('resolveCellRender', () => {
5557
exec: undefined,
5658
column: column('ttl'),
5759
waitingOnLabels: undefined,
60+
referenceColumnsEnabled: false,
5861
timeZone: 'America/Los_Angeles',
5962
timezoneStatus: 'loading',
6063
})
@@ -68,6 +71,7 @@ describe('resolveCellRender', () => {
6871
exec: undefined,
6972
column: column('date'),
7073
waitingOnLabels: undefined,
74+
referenceColumnsEnabled: false,
7175
timeZone: 'America/Los_Angeles',
7276
timezoneStatus: 'error',
7377
})
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
Button: ({
12+
children,
13+
size,
14+
variant,
15+
...props
16+
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
17+
size?: string
18+
variant?: string
19+
}) => (
20+
<button data-size={size} data-variant={variant} {...props}>
21+
{children}
22+
</button>
23+
),
24+
Checkbox: () => null,
25+
ChipTag: ({
26+
children,
27+
variant,
28+
...props
29+
}: React.HTMLAttributes<HTMLSpanElement> & { variant?: string }) => (
30+
<span data-chip-tag-variant={variant} {...props}>
31+
{children}
32+
</span>
33+
),
34+
cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' '),
35+
Tooltip: {
36+
Root: ({ children }: { children: React.ReactNode }) => children,
37+
Trigger: ({ children }: { children: React.ReactNode }) => children,
38+
Content: ({ children }: { children: React.ReactNode }) => children,
39+
},
40+
}))
41+
42+
vi.mock('@/app/workspace/[workspaceId]/logs/utils', () => ({
43+
StatusBadge: () => null,
44+
}))
45+
46+
vi.mock(
47+
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell',
48+
() => ({ SimResourceCell: () => null })
49+
)
50+
51+
vi.mock('@/app/workspace/[workspaceId]/tables/[tableId]/components/select-field', () => ({
52+
resolveSelectOptions: () => [],
53+
SelectPill: () => null,
54+
}))
55+
56+
import {
57+
CellRender,
58+
resolveCellRender,
59+
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render'
60+
61+
const REFERENCE_COLUMN: DisplayColumn = {
62+
id: 'col-account',
63+
key: 'col-account',
64+
name: 'Account',
65+
type: 'reference',
66+
referenceTableId: 'table-accounts',
67+
referenceTableName: 'Accounts',
68+
groupSize: 1,
69+
groupStartColIndex: 0,
70+
headerLabel: 'Account',
71+
isGroupStart: true,
72+
}
73+
74+
let container: HTMLDivElement
75+
let root: Root
76+
77+
beforeEach(() => {
78+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
79+
container = document.createElement('div')
80+
document.body.appendChild(container)
81+
act(() => {
82+
root = createRoot(container)
83+
})
84+
})
85+
86+
afterEach(() => {
87+
act(() => root.unmount())
88+
container.remove()
89+
})
90+
91+
describe('reference cell rendering', () => {
92+
it('resolves a stored row ID to a chip labeled with the referenced table name', () => {
93+
expect(
94+
resolveCellRender({
95+
value: 'row-account-1',
96+
exec: undefined,
97+
column: REFERENCE_COLUMN,
98+
waitingOnLabels: undefined,
99+
referenceColumnsEnabled: true,
100+
})
101+
).toEqual({ kind: 'reference-chip', label: 'Accounts' })
102+
})
103+
104+
it('keeps an empty reference cell empty', () => {
105+
expect(
106+
resolveCellRender({
107+
value: '',
108+
exec: undefined,
109+
column: REFERENCE_COLUMN,
110+
waitingOnLabels: undefined,
111+
referenceColumnsEnabled: true,
112+
})
113+
).toEqual({ kind: 'empty' })
114+
})
115+
116+
it('uses a neutral label while the referenced table name is unavailable', () => {
117+
expect(
118+
resolveCellRender({
119+
value: 'row-account-1',
120+
exec: undefined,
121+
column: { ...REFERENCE_COLUMN, referenceTableName: undefined },
122+
waitingOnLabels: undefined,
123+
referenceColumnsEnabled: true,
124+
})
125+
).toEqual({ kind: 'reference-chip', label: 'Referenced table' })
126+
})
127+
128+
it('renders the stored row ID as plain text when the feature is disabled', () => {
129+
expect(
130+
resolveCellRender({
131+
value: 'row-account-1',
132+
exec: undefined,
133+
column: REFERENCE_COLUMN,
134+
waitingOnLabels: undefined,
135+
referenceColumnsEnabled: false,
136+
})
137+
).toEqual({ kind: 'text', text: 'row-account-1' })
138+
})
139+
140+
it('opens the referenced row from the chip without exposing its stored row ID', () => {
141+
const onReferenceClick = vi.fn()
142+
143+
act(() => {
144+
root.render(
145+
<CellRender
146+
kind={resolveCellRender({
147+
value: 'row-account-1',
148+
exec: undefined,
149+
column: REFERENCE_COLUMN,
150+
waitingOnLabels: undefined,
151+
referenceColumnsEnabled: true,
152+
})}
153+
isEditing={false}
154+
referenceAction={{ expanded: false, onClick: onReferenceClick }}
155+
/>
156+
)
157+
})
158+
159+
const chip = container.querySelector('button')
160+
expect(chip?.textContent).toBe('Accounts')
161+
expect(chip?.dataset.variant).toBe('ghost')
162+
expect(chip?.dataset.size).toBe('sm')
163+
expect(chip).toHaveProperty('dataset.referenceCellTrigger', '')
164+
expect(chip?.className).toContain('max-w-full')
165+
expect(chip?.className).toContain('p-0')
166+
expect(chip?.querySelector('svg')).toBeNull()
167+
const tag = chip?.querySelector('[data-chip-tag-variant="field"]')
168+
expect(tag?.textContent).toBe('Accounts')
169+
expect(tag?.className).toContain('min-w-0')
170+
expect(tag?.className).toContain('max-w-full')
171+
172+
act(() => chip?.click())
173+
174+
expect(onReferenceClick).toHaveBeenCalledOnce()
175+
expect(container.textContent).not.toContain('row-account-1')
176+
})
177+
178+
it('keeps a chip double-click from reaching the reference cell', () => {
179+
const onCellDoubleClick = vi.fn()
180+
181+
act(() => {
182+
root.render(
183+
<div onDoubleClick={onCellDoubleClick}>
184+
<CellRender
185+
kind={{ kind: 'reference-chip', label: 'Accounts' }}
186+
isEditing={false}
187+
referenceAction={{ expanded: false, onClick: vi.fn() }}
188+
/>
189+
</div>
190+
)
191+
})
192+
193+
act(() => {
194+
container
195+
.querySelector('button')
196+
?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
197+
})
198+
199+
expect(onCellDoubleClick).not.toHaveBeenCalled()
200+
})
201+
})

0 commit comments

Comments
 (0)