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
71 changes: 71 additions & 0 deletions apps/sim/app/api/table/names/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @vitest-environment node
*/

import { createMockRequest } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const mocks = vi.hoisted(() => ({
getSession: vi.fn(),
listNames: vi.fn(),
}))

vi.mock('@/lib/auth', () => ({ getSession: mocks.getSession }))
vi.mock('@/lib/table/application/operations', () => ({
tableOperations: { list: { id: 'tables.list' } },
}))
vi.mock('@/lib/table/application/tables', () => ({
listTableNamesUseCase: { operation: { id: 'tables.list' }, execute: mocks.listNames },
}))

import { POST } from '@/app/api/table/names/route'

function request(body?: unknown) {
return createMockRequest('POST', body, {}, 'http://localhost/api/table/names')
}

describe('POST /api/table/names', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getSession.mockResolvedValue({
user: { id: 'user-1' },
session: { id: 'session-1' },
})
mocks.listNames.mockResolvedValue({
tables: [{ id: 'table-1', name: 'Accounts' }],
})
})

it('returns the lightweight table-name projection', async () => {
const response = await POST(
request({ workspaceId: 'workspace-1', tableIds: ['table-1', 'table-2'] }),
{}
)

expect(response.status).toBe(200)
expect(mocks.listNames.mock.calls[0][0]).toMatchObject({
principal: { kind: 'session', userId: 'user-1' },
input: { workspaceId: 'workspace-1', tableIds: ['table-1', 'table-2'] },
})
expect(await response.json()).toEqual({
success: true,
data: { tables: [{ id: 'table-1', name: 'Accounts' }] },
})
})

it('authenticates before validating the body', async () => {
mocks.getSession.mockResolvedValue(null)

const response = await POST(request(), {})

expect(response.status).toBe(401)
expect(mocks.listNames).not.toHaveBeenCalled()
})

it('rejects an empty table ID list', async () => {
const response = await POST(request({ workspaceId: 'workspace-1', tableIds: [] }), {})

expect(response.status).toBe(400)
expect(mocks.listNames).not.toHaveBeenCalled()
})
})
22 changes: 22 additions & 0 deletions apps/sim/app/api/table/names/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { listTableNamesContract } from '@/lib/api/contracts/tables'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { tableOperations } from '@/lib/table/application/operations'
import { listTableNamesUseCase } from '@/lib/table/application/tables'

export const POST = defineInternalJsonRoute({
contract: listTableNamesContract,
operation: tableOperations.list,
auth: internalSessionAuth,
rateLimit: internalRateLimits.none({
reason: 'Preserve existing internal table list behavior',
}),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ body }) => body,
useCase: listTableNamesUseCase,
present: ({ tables }) => ({ success: true as const, data: { tables } }),
})
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
'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 { TimezoneState } from '@/hooks/queries/general-settings'
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 @@ -16,6 +20,7 @@ interface CellContentProps {
workspaceId: string
timeZone: string
timezoneStatus: TimezoneState['status']
referenceColumnsEnabled: boolean
isEditing: boolean
initialCharacter?: string | null
onSave: (value: unknown, reason: SaveReason) => void
Expand All @@ -28,6 +33,7 @@ interface CellContentProps {
waitingOnLabels?: string[]
/** Column is an enrichment output — a completed-but-empty cell renders "Not found". */
isEnrichmentOutput?: boolean
referenceAction?: ReferenceCellAction
}

/**
Expand All @@ -43,12 +49,14 @@ export function CellContent({
workspaceId,
timeZone,
timezoneStatus,
referenceColumnsEnabled,
isEditing,
initialCharacter,
onSave,
onCancel,
waitingOnLabels,
isEnrichmentOutput,
referenceAction,
}: CellContentProps) {
const kind = resolveCellRender({
value,
Expand All @@ -59,6 +67,7 @@ export function CellContent({
currentWorkspaceId: workspaceId,
timeZone,
timezoneStatus,
referenceColumnsEnabled,
})

return (
Expand All @@ -74,7 +83,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
Expand Up @@ -30,6 +30,7 @@ describe('resolveCellRender', () => {
exec: undefined,
column: column('ttl'),
waitingOnLabels: undefined,
referenceColumnsEnabled: false,
timeZone: 'America/New_York',
})
).toEqual({ kind: 'date', text: '2023-11-14T17:13:20-05:00' })
Expand All @@ -42,6 +43,7 @@ describe('resolveCellRender', () => {
exec: undefined,
column: column('ttl'),
waitingOnLabels: undefined,
referenceColumnsEnabled: false,
timeZone: 'America/Los_Angeles',
timezoneStatus: 'invalid',
})
Expand All @@ -55,6 +57,7 @@ describe('resolveCellRender', () => {
exec: undefined,
column: column('ttl'),
waitingOnLabels: undefined,
referenceColumnsEnabled: false,
timeZone: 'America/Los_Angeles',
timezoneStatus: 'loading',
})
Expand All @@ -68,6 +71,7 @@ describe('resolveCellRender', () => {
exec: undefined,
column: column('date'),
waitingOnLabels: undefined,
referenceColumnsEnabled: false,
timeZone: 'America/Los_Angeles',
timezoneStatus: 'error',
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* @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,
referenceColumnsEnabled: true,
})
).toEqual({ kind: 'reference-chip', label: 'Accounts' })
})

it('keeps an empty reference cell empty', () => {
expect(
resolveCellRender({
value: '',
exec: undefined,
column: REFERENCE_COLUMN,
waitingOnLabels: undefined,
referenceColumnsEnabled: true,
})
).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,
referenceColumnsEnabled: true,
})
).toEqual({ kind: 'reference-chip', label: 'Referenced table' })
})

it('renders the stored row ID as plain text when the feature is disabled', () => {
expect(
resolveCellRender({
value: 'row-account-1',
exec: undefined,
column: REFERENCE_COLUMN,
waitingOnLabels: undefined,
referenceColumnsEnabled: false,
})
).toEqual({ kind: 'text', text: 'row-account-1' })
})

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,
referenceColumnsEnabled: true,
})}
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).toHaveProperty('dataset.referenceCellTrigger', '')
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()
const onReferenceClick = vi.fn()

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

act(() => {
const chip = container.querySelector('button')
chip?.dispatchEvent(new MouseEvent('click', { bubbles: true, detail: 1 }))
chip?.dispatchEvent(new MouseEvent('click', { bubbles: true, detail: 2 }))
chip?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, detail: 2 }))
})

expect(onReferenceClick).toHaveBeenCalledOnce()
expect(onCellDoubleClick).not.toHaveBeenCalled()
})
})
Loading