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
Expand Up @@ -48,11 +48,7 @@ vi.mock('@sim/emcn', () => ({
},
ChipInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
FieldDivider: () => <hr />,
Label: ({ children, ...props }: React.LabelHTMLAttributes<HTMLLabelElement>) => (
<label htmlFor={props.htmlFor ?? 'test-field'} {...props}>
{children}
</label>
),
Label: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
Switch: ({ checked }: { checked?: boolean }) => (
<button type='button' aria-pressed={checked}>
Toggle
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* @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 { WorkflowGroup } from '@/lib/table'
import type { DisplayColumn } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/types'

vi.mock('@sim/emcn', () => ({
cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' '),
}))

vi.mock('@sim/emcn/icons', () => ({
ChevronDown: () => null,
}))

vi.mock(
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-type-icon',
() => ({ ColumnTypeIcon: () => null })
)

vi.mock(
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/header-label',
() => ({ HeaderLabel: ({ label }: { label: string }) => <span>{label}</span> })
)

vi.mock(
'@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell',
() => ({ ColumnOptionsMenu: () => null })
)

import { ColumnHeaderMenu } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu'

let container: HTMLDivElement
let root: Root

const DEFAULT_COLUMN: DisplayColumn = {
id: 'col-name',
key: 'col-name',
name: 'Name',
type: 'string',
groupSize: 1,
groupStartColIndex: 0,
headerLabel: 'Name',
isGroupStart: true,
}

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()
})

function renderHeader({
column = DEFAULT_COLUMN,
workflowGroups,
onColumnSelect = vi.fn(),
onOpenConfig = vi.fn(),
onRenameColumn = vi.fn(),
}: {
column?: DisplayColumn
workflowGroups?: WorkflowGroup[]
onColumnSelect?: (colIndex: number, shiftKey: boolean) => void
onOpenConfig?: (columnName: string) => void
onRenameColumn?: (columnName: string) => void
} = {}) {
act(() => {
root.render(
<table>
<thead>
<tr>
<ColumnHeaderMenu
column={column}
colIndex={2}
isRenaming={false}
isColumnSelected={false}
renameValue=''
onRenameValueChange={vi.fn()}
onRenameSubmit={vi.fn()}
onRenameCancel={vi.fn()}
onColumnSelect={onColumnSelect}
onInsertLeft={vi.fn()}
onInsertRight={vi.fn()}
onRenameColumn={onRenameColumn}
onDeleteColumn={vi.fn()}
onResizeStart={vi.fn()}
onResize={vi.fn()}
onResizeEnd={vi.fn()}
onAutoResize={vi.fn()}
onOpenConfig={onOpenConfig}
workflowGroups={workflowGroups}
/>
</tr>
</thead>
</table>
)
})

const headerButton = Array.from(container.querySelectorAll('button')).find((button) =>
button.textContent?.includes(column.workflowGroupId ? column.headerLabel : column.name)
)
if (!headerButton) throw new Error('Column header button was not rendered')
return headerButton
}

describe('ColumnHeaderMenu interactions', () => {
it('selects the column without opening configuration on a single click', () => {
const onColumnSelect = vi.fn()
const onOpenConfig = vi.fn()
const onRenameColumn = vi.fn()
const headerButton = renderHeader({ onColumnSelect, onOpenConfig, onRenameColumn })

act(() => headerButton.click())

expect(onColumnSelect).toHaveBeenCalledWith(2, false)
expect(onOpenConfig).not.toHaveBeenCalled()
expect(onRenameColumn).not.toHaveBeenCalled()
})

it('selects before starting inline rename on a double click', () => {
const onColumnSelect = vi.fn()
const onRenameColumn = vi.fn()
const headerButton = renderHeader({ onColumnSelect, onRenameColumn })

act(() => {
headerButton.click()
headerButton.click()
headerButton.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
})

expect(onColumnSelect).toHaveBeenCalledTimes(2)
expect(onRenameColumn).toHaveBeenCalledWith('col-name')
})

it('does not rename a workflow-output column on double click', () => {
const onRenameColumn = vi.fn()
const headerButton = renderHeader({
column: { ...DEFAULT_COLUMN, workflowGroupId: 'workflow-group' },
workflowGroups: [
{
id: 'workflow-group',
workflowId: 'workflow-1',
type: 'manual',
outputs: [{ blockId: 'block-1', path: 'result', columnName: 'col-name' }],
},
],
onRenameColumn,
})

act(() => {
headerButton.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
})

expect(onRenameColumn).not.toHaveBeenCalled()
})

it('renames an enrichment column on double click', () => {
const onRenameColumn = vi.fn()
const headerButton = renderHeader({
column: { ...DEFAULT_COLUMN, workflowGroupId: 'enrichment-group' },
workflowGroups: [
{
id: 'enrichment-group',
workflowId: '',
enrichmentId: 'company-domain',
type: 'enrichment',
outputs: [{ blockId: '', path: '', outputId: 'domain', columnName: 'col-name' }],
},
],
onRenameColumn,
})

act(() => {
headerButton.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
})

expect(onRenameColumn).toHaveBeenCalledWith('col-name')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ interface ColumnHeaderMenuProps {
isRenaming: boolean
isColumnSelected: boolean
renameValue: string
/** Marks a refused inline rename until the user changes or cancels it. */
renameError?: boolean
onRenameValueChange: (value: string) => void
onRenameSubmit: () => void
onRenameCancel: () => void
onColumnSelect: (colIndex: number, shiftKey: boolean) => void
onInsertLeft: (columnName: string) => void
onInsertRight: (columnName: string) => void
/** Starts inline renaming when a plain or enrichment header is double-clicked. */
onRenameColumn?: (columnName: string) => void
/** Opens the table targeted by a Reference column. */
onGoToReferenceTable?: (tableId: string) => void
onDeleteColumn: (columnName: string) => void
Expand Down Expand Up @@ -70,12 +74,14 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
isRenaming,
isColumnSelected,
renameValue,
renameError,
onRenameValueChange,
onRenameSubmit,
onRenameCancel,
onColumnSelect,
onInsertLeft,
onInsertRight,
onRenameColumn,
onGoToReferenceTable,
onDeleteColumn,
onResizeStart,
Expand Down Expand Up @@ -118,6 +124,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
? 'Hide column'
: 'Delete column'
: undefined
const isWorkflowOutput = Boolean(column.workflowGroupId && ownGroup?.type !== 'enrichment')
useEffect(() => {
if (isRenaming && renameInputRef.current) {
renameInputRef.current.focus()
Expand Down Expand Up @@ -228,9 +235,11 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
}
if (isRenaming) return
onColumnSelect(colIndex, e.shiftKey)
if (!e.shiftKey) {
onOpenConfig(column.key)
}
}

function handleHeaderDoubleClick() {
if (isRenaming || isWorkflowOutput) return
onRenameColumn?.(column.key)
}

function handleChevronClick(e: React.MouseEvent) {
Expand Down Expand Up @@ -298,7 +307,11 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
if (e.key === 'Escape') onRenameCancel()
}}
onBlur={onRenameSubmit}
className='ml-1.5 min-w-0 flex-1 border-0 bg-transparent p-0 text-[var(--text-primary)] text-small outline-none focus:outline-none focus:ring-0'
aria-invalid={renameError || undefined}
className={cn(
'ml-1.5 min-w-0 flex-1 border-0 bg-transparent p-0 text-small outline-none focus:outline-none focus:ring-0',
renameError ? 'text-[var(--text-error)]' : 'text-[var(--text-primary)]'
)}
/>
</div>
) : readOnly ? (
Expand All @@ -320,6 +333,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
type='button'
className='flex min-w-0 flex-1 cursor-pointer items-center px-2 py-[7px] outline-none'
onClick={handleHeaderClick}
onDoubleClick={handleHeaderDoubleClick}
draggable={false}
>
<ColumnTypeIcon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ vi.mock('@sim/emcn/icons', () => ({
Pin: () => null,
PinOff: () => null,
PlayOutline: () => null,
Settings: () => null,
SquareArrowUpRight: () => null,
Trash: () => null,
Workflow: () => null,
Expand Down Expand Up @@ -92,6 +91,7 @@ function renderMenu(column: ColumnDefinition, onGoToReferenceTable: (tableId: st
onInsertLeft={vi.fn()}
onInsertRight={vi.fn()}
onDeleteColumn={vi.fn()}
onOpenConfig={vi.fn()}
onGoToReferenceTable={onGoToReferenceTable}
/>
)
Expand Down Expand Up @@ -134,3 +134,11 @@ describe('ColumnOptionsMenu Reference navigation', () => {
expect(findButton('Go to Reference Table')).toBeUndefined()
})
})

describe('ColumnOptionsMenu editing', () => {
it('keeps rename out of the column menu', () => {
renderMenu({ id: 'col-name', name: 'Name', type: 'string' }, vi.fn())

expect(findButton('Rename column')).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ interface ColumnOptionsMenuProps {
/**
* Shared column-options dropdown rendered next to the column header chevron
* AND on right-click of the workflow group meta cell. Anchors to a fixed
* position passed in (so callers can place it under the chevron, or at the
* cursor for context-menu use). Rename / change type / unique live in the
* column sidebar (opened by Edit column).
* position passed in so callers can place it under the chevron or at the
* cursor. Rename starts in the header; type, uniqueness, and type-specific
* configuration live in the sidebar opened by Edit column.
*/
export function ColumnOptionsMenu({
open,
Expand Down
Loading