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
@@ -0,0 +1,70 @@
/**
* @vitest-environment jsdom
*/
import { act } from 'react'
import { createTableColumn } from '@sim/testing'
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(
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render',
() => ({
resolveCellRender: () => ({ kind: 'empty' }),
CellRender: () => null,
})
)

vi.mock(
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/inline-editors',
() => ({ InlineEditor: () => <input data-testid='inline-editor' /> })
)

import { CellContent } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content'

const COLUMN: DisplayColumn = {
...createTableColumn({ id: 'col-name', name: 'Name', type: 'string' }),
key: 'col-name',
groupSize: 1,
groupStartColIndex: 0,
headerLabel: 'Name',
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('CellContent', () => {
it('keeps the inline editor below the sticky table header', () => {
act(() => {
root.render(
<CellContent
value='Acme'
column={COLUMN}
workspaceId='workspace-1'
isEditing
onSave={vi.fn()}
onCancel={vi.fn()}
/>
)
})

const editorLayer = container.querySelector('[data-testid="inline-editor"]')?.parentElement
expect(editorLayer?.className).toContain('z-[9]')
expect(editorLayer?.className).not.toContain('z-10')
})
})
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 @@ -57,7 +64,7 @@ export function CellContent({
return (
<>
{isEditing && (
<div className='absolute inset-0 z-10 flex items-center px-0'>
<div className='absolute inset-0 z-[9] flex items-center px-0'>
<InlineEditor
value={value}
column={column}
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,184 @@
/**
* @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>,
Button: ({
children,
size,
variant,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
size?: string
variant?: string
}) => (
<button data-size={size} data-variant={variant} {...props}>
{children}
</button>
),
Checkbox: () => null,
ChipTag: ({
children,
variant,
...props
}: React.HTMLAttributes<HTMLSpanElement> & { variant?: string }) => (
<span data-chip-tag-variant={variant} {...props}>
{children}
</span>
),
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',
referenceTableName: '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 referenced table name', () => {
expect(
resolveCellRender({
value: 'row-account-1',
exec: undefined,
column: REFERENCE_COLUMN,
waitingOnLabels: undefined,
})
).toEqual({ kind: 'column-chip', label: 'Accounts' })
})

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

it('uses a neutral label while the referenced table name is unavailable', () => {
expect(
resolveCellRender({
value: 'row-account-1',
exec: undefined,
column: { ...REFERENCE_COLUMN, referenceTableName: undefined },
waitingOnLabels: undefined,
})
).toEqual({ kind: 'column-chip', label: 'Referenced table' })
})

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('Accounts')
expect(chip?.dataset.variant).toBe('ghost')
expect(chip?.dataset.size).toBe('sm')
expect(chip?.className).toContain('max-w-full')
expect(chip?.className).toContain('p-0')
expect(chip?.querySelector('svg')).toBeNull()
const tag = chip?.querySelector('[data-chip-tag-variant="field"]')
expect(tag?.textContent).toBe('Accounts')
expect(tag?.className).toContain('min-w-0')
expect(tag?.className).toContain('max-w-full')

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

expect(onReferenceClick).toHaveBeenCalledOnce()
expect(container.textContent).not.toContain('row-account-1')
})

it('keeps a chip double-click from reaching the reference cell', () => {
const onCellDoubleClick = vi.fn()

act(() => {
root.render(
<div onDoubleClick={onCellDoubleClick}>
<CellRender
kind={{ kind: 'column-chip', label: 'Accounts' }}
isEditing={false}
referenceAction={{ expanded: false, onClick: vi.fn() }}
/>
</div>
)
})

act(() => {
container
.querySelector('button')
?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
})

expect(onCellDoubleClick).not.toHaveBeenCalled()
})
})
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, Button, Checkbox, ChipTag, 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 }
| { kind: 'json'; text: string }
| { kind: 'date'; text: string }
| { kind: 'url'; text: string; href: string; domain: string }
Expand Down Expand Up @@ -128,6 +129,16 @@ 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: column.referenceTableName ?? 'Referenced table',
}
: { 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 +262,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 +396,26 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
</span>
)

case 'column-chip':
return (
<Button
variant='ghost'
size='sm'
aria-expanded={referenceAction?.expanded}
disabled={!referenceAction}
className={cn('min-w-0 max-w-full p-0', isEditing && 'invisible')}
onClick={(event) => {
event.stopPropagation()
referenceAction?.onClick()
}}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
onDoubleClick={(event) => event.stopPropagation()}
>
<ChipTag variant='field' className='min-w-0 max-w-full'>
<span className='truncate'>{kind.label}</span>
</ChipTag>
</Button>
)

case 'json':
return (
<span
Expand Down
Loading
Loading