Skip to content

Commit b734463

Browse files
committed
improvement(tables): polish reference row previews
1 parent fabb273 commit b734463

15 files changed

Lines changed: 795 additions & 109 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createTableColumn } from '@sim/testing'
6+
import { createRoot, type Root } from 'react-dom/client'
7+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
8+
import type { DisplayColumn } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/types'
9+
10+
vi.mock(
11+
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render',
12+
() => ({
13+
resolveCellRender: () => ({ kind: 'empty' }),
14+
CellRender: () => null,
15+
})
16+
)
17+
18+
vi.mock(
19+
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors',
20+
() => ({ InlineEditor: () => <input data-testid='inline-editor' /> })
21+
)
22+
23+
import { CellContent } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content'
24+
25+
const COLUMN: DisplayColumn = {
26+
...createTableColumn({ id: 'col-name', name: 'Name', type: 'string' }),
27+
key: 'col-name',
28+
groupSize: 1,
29+
groupStartColIndex: 0,
30+
headerLabel: 'Name',
31+
isGroupStart: true,
32+
}
33+
34+
let container: HTMLDivElement
35+
let root: Root
36+
37+
beforeEach(() => {
38+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
39+
container = document.createElement('div')
40+
document.body.appendChild(container)
41+
act(() => {
42+
root = createRoot(container)
43+
})
44+
})
45+
46+
afterEach(() => {
47+
act(() => root.unmount())
48+
container.remove()
49+
})
50+
51+
describe('CellContent', () => {
52+
it('keeps the inline editor below the sticky table header', () => {
53+
act(() => {
54+
root.render(
55+
<CellContent
56+
value='Acme'
57+
column={COLUMN}
58+
workspaceId='workspace-1'
59+
isEditing
60+
onSave={vi.fn()}
61+
onCancel={vi.fn()}
62+
/>
63+
)
64+
})
65+
66+
const editorLayer = container.querySelector('[data-testid="inline-editor"]')?.parentElement
67+
expect(editorLayer?.className).toContain('z-[9]')
68+
expect(editorLayer?.className).not.toContain('z-10')
69+
})
70+
})

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function CellContent({
6464
return (
6565
<>
6666
{isEditing && (
67-
<div className='absolute inset-0 z-10 flex items-center px-0'>
67+
<div className='absolute inset-0 z-[9] flex items-center px-0'>
6868
<InlineEditor
6969
value={value}
7070
column={column}

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

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,28 @@ import type { DisplayColumn } from '@/app/workspace/[workspaceId]/tables/[tableI
88

99
vi.mock('@sim/emcn', () => ({
1010
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+
),
1124
Checkbox: () => null,
12-
Chip: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
13-
<button {...props}>{children}</button>
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>
1433
),
1534
cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' '),
1635
Tooltip: {
@@ -45,6 +64,7 @@ const REFERENCE_COLUMN: DisplayColumn = {
4564
name: 'Account',
4665
type: 'reference',
4766
referenceTableId: 'table-accounts',
67+
referenceTableName: 'Accounts',
4868
groupSize: 1,
4969
groupStartColIndex: 0,
5070
headerLabel: 'Account',
@@ -69,15 +89,15 @@ afterEach(() => {
6989
})
7090

7191
describe('reference cell rendering', () => {
72-
it('resolves a stored row ID to a chip labeled with the reference column name', () => {
92+
it('resolves a stored row ID to a chip labeled with the referenced table name', () => {
7393
expect(
7494
resolveCellRender({
7595
value: 'row-account-1',
7696
exec: undefined,
7797
column: REFERENCE_COLUMN,
7898
waitingOnLabels: undefined,
7999
})
80-
).toMatchObject({ kind: 'column-chip', label: 'Account' })
100+
).toEqual({ kind: 'column-chip', label: 'Accounts' })
81101
})
82102

83103
it('keeps an empty reference cell empty', () => {
@@ -91,6 +111,17 @@ describe('reference cell rendering', () => {
91111
).toEqual({ kind: 'empty' })
92112
})
93113

114+
it('uses a neutral label while the referenced table name is unavailable', () => {
115+
expect(
116+
resolveCellRender({
117+
value: 'row-account-1',
118+
exec: undefined,
119+
column: { ...REFERENCE_COLUMN, referenceTableName: undefined },
120+
waitingOnLabels: undefined,
121+
})
122+
).toEqual({ kind: 'column-chip', label: 'Referenced table' })
123+
})
124+
94125
it('opens the referenced row from the chip without exposing its stored row ID', () => {
95126
const onReferenceClick = vi.fn()
96127

@@ -110,11 +141,44 @@ describe('reference cell rendering', () => {
110141
})
111142

112143
const chip = container.querySelector('button')
113-
expect(chip?.textContent).toBe('Account')
144+
expect(chip?.textContent).toBe('Accounts')
145+
expect(chip?.dataset.variant).toBe('ghost')
146+
expect(chip?.dataset.size).toBe('sm')
147+
expect(chip?.className).toContain('max-w-full')
148+
expect(chip?.className).toContain('p-0')
149+
expect(chip?.querySelector('svg')).toBeNull()
150+
const tag = chip?.querySelector('[data-chip-tag-variant="field"]')
151+
expect(tag?.textContent).toBe('Accounts')
152+
expect(tag?.className).toContain('min-w-0')
153+
expect(tag?.className).toContain('max-w-full')
114154

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

117157
expect(onReferenceClick).toHaveBeenCalledOnce()
118158
expect(container.textContent).not.toContain('row-account-1')
119159
})
160+
161+
it('keeps a chip double-click from reaching the reference cell', () => {
162+
const onCellDoubleClick = vi.fn()
163+
164+
act(() => {
165+
root.render(
166+
<div onDoubleClick={onCellDoubleClick}>
167+
<CellRender
168+
kind={{ kind: 'column-chip', label: 'Accounts' }}
169+
isEditing={false}
170+
referenceAction={{ expanded: false, onClick: vi.fn() }}
171+
/>
172+
</div>
173+
)
174+
})
175+
176+
act(() => {
177+
container
178+
.querySelector('button')
179+
?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
180+
})
181+
182+
expect(onCellDoubleClick).not.toHaveBeenCalled()
183+
})
120184
})

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

Lines changed: 13 additions & 13 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, Chip, cn, Tooltip } from '@sim/emcn'
5+
import { Badge, Button, Checkbox, ChipTag, 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,7 +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 }> }
31+
| { kind: 'column-chip'; label: string }
3232
| { kind: 'json'; text: string }
3333
| { kind: 'date'; text: string }
3434
| { kind: 'url'; text: string; href: string; domain: string }
@@ -135,8 +135,7 @@ export function resolveCellRender({
135135
return rowId
136136
? {
137137
kind: 'column-chip',
138-
label: typeDefinition.referencePreview.getChipLabel(column),
139-
icon: typeDefinition.icon,
138+
label: column.referenceTableName ?? 'Referenced table',
140139
}
141140
: { kind: 'empty' }
142141
}
@@ -397,24 +396,25 @@ export function CellRender({
397396
</span>
398397
)
399398

400-
case 'column-chip': {
401-
const ChipIcon = kind.icon
399+
case 'column-chip':
402400
return (
403-
<Chip
404-
active={referenceAction?.expanded}
405-
leftIcon={ChipIcon}
401+
<Button
402+
variant='ghost'
403+
size='sm'
406404
aria-expanded={referenceAction?.expanded}
407405
disabled={!referenceAction}
408-
className={cn('h-5 max-w-full', isEditing && 'invisible')}
406+
className={cn('min-w-0 max-w-full p-0', isEditing && 'invisible')}
409407
onClick={(event) => {
410408
event.stopPropagation()
411409
referenceAction?.onClick()
412410
}}
411+
onDoubleClick={(event) => event.stopPropagation()}
413412
>
414-
{kind.label}
415-
</Chip>
413+
<ChipTag variant='field' className='min-w-0 max-w-full'>
414+
<span className='truncate'>{kind.label}</span>
415+
</ChipTag>
416+
</Button>
416417
)
417-
}
418418

419419
case 'json':
420420
return (

0 commit comments

Comments
 (0)