Skip to content
Closed
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@ export const COLUMN_TYPE_OPTIONS: ColumnTypeOption[] = [
{ type: 'workflow', label: 'Workflow', icon: PlayOutline },
]

/** Plain column types (no workflow). Used by `<ColumnConfigSidebar>`'s type combobox in edit mode. */
export const PLAIN_COLUMN_TYPE_OPTIONS = COLUMN_TYPE_OPTIONS.filter((o) => o.type !== 'workflow')
/** Plain column types (no workflow). Used by the column header menu's "Change type" submenu. */
export const PLAIN_COLUMN_TYPE_OPTIONS: (ColumnTypeOption & {
type: ColumnDefinition['type']
})[] = COLUMN_TYPE_OPTIONS.filter(
(o): o is ColumnTypeOption & { type: ColumnDefinition['type'] } => o.type !== 'workflow'
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { useRef } from 'react'
import {
ChipChevronDown,
chipContentIconClass,
Expand Down Expand Up @@ -39,8 +40,10 @@ interface NewColumnDropdownProps {

/**
* "+ New column" dropdown — the single entry point for creating a column.
* Lists every column type plus "Workflow" and "Enrichments"; picking a type
* opens the right sidebar pre-seeded.
* Lists every column type plus "Workflow" and "Enrichments". Picking a scalar
* type adds a draft header cell for naming — nothing persists until the name
* commits (committing a select's name opens its options sidebar, and it
* persists from there). Workflow and Enrichments open their own sidebars.
*/
export function NewColumnDropdown({
trigger,
Expand All @@ -51,6 +54,8 @@ export function NewColumnDropdown({
blocked,
onBlocked,
}: NewColumnDropdownProps) {
const pendingTypeRef = useRef<ColumnDefinition['type'] | null>(null)

const triggerButton =
trigger === 'header' ? (
<button
Expand Down Expand Up @@ -90,7 +95,25 @@ export function NewColumnDropdown({
(295px with its separator and padding), so the default cut the last
two off behind a scrollbar. Sized here rather than in the shared
component, which every other dropdown in the app relies on. */}
<DropdownMenuContent align='start' side='bottom' sideOffset={4} className='max-h-[320px]'>
{/* A type pick is deferred to here, the moment the menu has fully
unmounted. Started from `onSelect`, the draft header's name input
would mount while this menu is still playing its exit animation —
and as the content zooms away from under the pointer, Radix's
item-leave handler focuses the closing menu, stealing the input's
focus mid-keystroke. The default close behavior (refocusing the
trigger) is prevented for the same reason. */}
<DropdownMenuContent
align='start'
side='bottom'
sideOffset={4}
className='max-h-[320px]'
onCloseAutoFocus={(e) => {
e.preventDefault()
const type = pendingTypeRef.current
pendingTypeRef.current = null
if (type) onPickType(type)
}}
>
<>
<DropdownMenuItem onSelect={onPickEnrichment}>
<Sparkles className='size-[14px] text-[var(--text-icon)]' />
Expand All @@ -103,7 +126,9 @@ export function NewColumnDropdown({
const onSelect =
option.type === 'workflow'
? onPickWorkflow
: () => onPickType(option.type as ColumnDefinition['type'])
: () => {
pendingTypeRef.current = option.type as ColumnDefinition['type']
}
return (
<DropdownMenuItem key={option.type} onSelect={onSelect}>
<Icon className='size-[14px] text-[var(--text-icon)]' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { cn } from '@sim/emcn'
import { ChevronDown } from '@sim/emcn/icons'
import type { SortDirection, WorkflowGroup } from '@/lib/table'
import type { ColumnDefinition, SortDirection, WorkflowGroup } from '@/lib/table'
import { columnTypeOf } from '@/lib/table/column-types'
import { HeaderLabel } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/header-label'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
import { COL_WIDTH, SELECTION_TINT_BG } from '../constants'
Expand All @@ -18,12 +19,24 @@ interface ColumnHeaderMenuProps {
isRenaming: boolean
isColumnSelected: boolean
renameValue: string
renameError?: boolean
onRenameValueChange: (value: string) => void
onRenameSubmit: () => void
onRenameCancel: () => void
onColumnSelect: (colIndex: number, shiftKey: boolean) => void
onInsertLeft: (columnName: string) => void
onInsertRight: (columnName: string) => void
/** Flip the column's `unique` constraint. Only forwarded to the menu for
* columns whose type supports it (registry `supportsUnique`) and that are
* not workflow outputs. */
onToggleUnique?: (columnName: string) => void
/** Starts the inline header rename. Forwarded for plain/enrichment columns;
* workflow outputs rename through the workflow sidebar. */
onRenameColumn?: (columnName: string) => void
/** Converts the column to another type. Plain/enrichment columns only. */
onChangeType?: (columnName: string, type: ColumnDefinition['type']) => void
/** Opens the config sidebar for types with per-column configuration. */
onConfigure?: (columnName: string) => void
onDeleteColumn: (columnName: string) => void
onResizeStart: (columnKey: string) => void
onResize: (columnKey: string, width: number) => void
Expand Down Expand Up @@ -68,12 +81,17 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
isRenaming,
isColumnSelected,
renameValue,
renameError,
onRenameValueChange,
onRenameSubmit,
onRenameCancel,
onColumnSelect,
onInsertLeft,
onInsertRight,
onToggleUnique,
onRenameColumn,
onChangeType,
onConfigure,
onDeleteColumn,
onResizeStart,
onResize,
Expand Down Expand Up @@ -115,6 +133,11 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
? 'Hide column'
: 'Delete column'
: undefined
// Workflow outputs never take a unique constraint; enrichment outputs behave
// like plain columns (matching `handleConfigureColumn`'s routing). The type's
// own say-so comes from the registry, never a per-type check here.
const isWorkflowOutput = !!column.workflowGroupId && ownGroup?.type !== 'enrichment'
const supportsUnique = !isWorkflowOutput && columnTypeOf(column).supportsUnique
useEffect(() => {
if (isRenaming && renameInputRef.current) {
renameInputRef.current.focus()
Expand Down Expand Up @@ -225,7 +248,10 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
}
if (isRenaming) return
onColumnSelect(colIndex, e.shiftKey)
if (!e.shiftKey) {
// Only workflow-output columns still have a config surface behind a plain
// click (the workflow sidebar). Plain columns edit inline / via the menu,
// so clicking their header just selects the column.
if (!e.shiftKey && isWorkflowOutput) {
onOpenConfig(column.key)
}
}
Expand Down Expand Up @@ -295,7 +321,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 Down Expand Up @@ -345,9 +375,13 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({
position={menuPosition}
column={column}
deleteLabel={deleteLabel}
onOpenConfig={onOpenConfig}
onOpenConfig={isWorkflowOutput ? onOpenConfig : undefined}
onRenameColumn={isWorkflowOutput ? undefined : onRenameColumn}
onChangeType={isWorkflowOutput ? undefined : onChangeType}
onConfigure={isWorkflowOutput ? undefined : onConfigure}
onInsertLeft={onInsertLeft}
onInsertRight={onInsertRight}
onToggleUnique={supportsUnique ? onToggleUnique : undefined}
onDeleteColumn={onDeleteColumn}
onViewWorkflow={
onViewWorkflow && ownGroup ? () => onViewWorkflow(ownGroup.workflowId) : undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client'

import React, { useEffect, useRef } from 'react'
import { cn } from '@sim/emcn'
import type { ColumnDefinition } from '@/lib/table'
import { ColumnTypeIcon } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-type-icon'

interface DraftColumnHeaderProps {
type: ColumnDefinition['type']
name: string
/** True after a refused commit — paints the name red until it's edited. */
invalid: boolean
onNameChange: (name: string) => void
onCommit: () => void
onCancel: () => void
}

/**
* Header cell for a column that exists only in this browser: the user picked
* a type and is naming it, but nothing is persisted until the name commits
* (or, for a type with configuration, until the sidebar saves). Renders like
* the rename state of a real header so the draft reads as "the column, being
* named" rather than a form. Like the "+ New column" cell it has no body
* cells beneath it.
*/
export const DraftColumnHeader = React.memo(function DraftColumnHeader({
type,
name,
invalid,
onNameChange,
onCommit,
onCancel,
}: DraftColumnHeaderProps) {
const inputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
inputRef.current?.focus()
inputRef.current?.select()
}, [])

return (
<th className='relative border-[var(--border)] border-r border-b bg-[var(--bg)] p-0 text-left align-middle'>
<div className='flex h-full w-full min-w-0 items-center px-2 py-[7px]'>
<ColumnTypeIcon type={type} />
<input
ref={inputRef}
type='text'
value={name}
aria-label='New column name'
aria-invalid={invalid || undefined}
onChange={(e) => onNameChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') onCommit()
if (e.key === 'Escape') onCancel()
}}
onBlur={onCommit}
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',
invalid ? 'text-[var(--text-error)]' : 'text-[var(--text-primary)]'
)}
/>
</div>
</th>
)
})
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { ColumnHeaderMenu } from './column-header-menu'
export { ColumnTypeIcon, columnTypeIcon } from './column-type-icon'
export { DraftColumnHeader } from './draft-column-header'
export { ColumnOptionsMenu, WorkflowGroupMetaCell } from './workflow-group-meta-cell'
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,20 @@ import {
ArrowUp,
Eye,
EyeOff,
Fingerprint,
Pencil,
Pin,
PinOff,
PlayOutline,
Settings,
Trash,
Workflow,
X,
} from '@sim/emcn/icons'
import type { RunLimit, RunMode } from '@/lib/api/contracts/tables'
import type { SortDirection, WorkflowGroupType } from '@/lib/table'
import type { ColumnDefinition, SortDirection, WorkflowGroupType } from '@/lib/table'
import { columnTypeOf } from '@/lib/table/column-types'
import { PLAIN_COLUMN_TYPE_OPTIONS } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar'
import { HeaderLabel } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/header-label'
import { getEnrichment } from '@/enrichments/registry'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
Expand Down Expand Up @@ -69,9 +73,23 @@ interface ColumnOptionsMenuProps {
* destructive action is non-lossy (workflow-output column where removing
* it leaves the group with siblings). */
deleteLabel?: string
onOpenConfig: (columnName: string) => void
/** Renders the "Edit column" item. Only workflow-owned columns still have a
* config surface behind it (the workflow sidebar); plain columns edit
* name/type/unique from this menu directly and omit it. */
onOpenConfig?: (columnName: string) => void
onRenameColumn?: (columnName: string) => void
/** Converts the column to `type`. Callers route `select` conversions through
* the config sidebar (the option set must be collected first). */
onChangeType?: (columnName: string, type: ColumnDefinition['type']) => void
/** Opens the config sidebar for a type with per-column configuration
* (registry `hasConfiguration`) — a select's options, a currency's code. */
onConfigure?: (columnName: string) => void
onInsertLeft: (columnName: string) => void
onInsertRight: (columnName: string) => void
/** Flip the column's `unique` constraint. Callers pass it only for columns
* that can carry one (plain/enrichment columns of a `supportsUnique` type),
* so the item's presence is the capability check. */
onToggleUnique?: (columnName: string) => void
onDeleteColumn: (columnName: string) => void
/** When provided (i.e. menu opened from a workflow-group meta header), the
* "Delete" item deletes the entire workflow group rather than the single
Expand Down Expand Up @@ -112,8 +130,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).
* cursor for context-menu use). Rename, change type, and the unique
* constraint are handled from here; per-type configuration and workflow
* outputs open their sidebars.
*/
export function ColumnOptionsMenu({
open,
Expand All @@ -122,8 +141,12 @@ export function ColumnOptionsMenu({
column,
deleteLabel,
onOpenConfig,
onRenameColumn,
onChangeType,
onConfigure,
onInsertLeft,
onInsertRight,
onToggleUnique,
onDeleteColumn,
onDeleteGroup,
onRunColumnAll,
Expand All @@ -142,6 +165,8 @@ export function ColumnOptionsMenu({
const showRunActions = Boolean(onRunColumnAll && onRunColumnIncomplete)
const showRunSelected = Boolean(onRunColumnSelected) && selectedRowCount > 0
const runLabels = runMenuLabels(hasActiveFilter)
const typeDefinition = columnTypeOf(column)
const CurrentTypeIcon = typeDefinition.icon
return (
<DropdownMenu open={open} onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
Expand Down Expand Up @@ -228,10 +253,53 @@ export function ColumnOptionsMenu({
View workflow
</DropdownMenuItem>
)}
<DropdownMenuItem onSelect={() => onOpenConfig(column.key)}>
<Pencil />
Edit column
</DropdownMenuItem>
{onOpenConfig && (
<DropdownMenuItem onSelect={() => onOpenConfig(column.key)}>
<Pencil />
Edit column
</DropdownMenuItem>
)}
{onRenameColumn && (
<DropdownMenuItem onSelect={() => onRenameColumn(column.key)}>
<Pencil />
Rename column
</DropdownMenuItem>
)}
{onChangeType && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<CurrentTypeIcon />
Change type
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{PLAIN_COLUMN_TYPE_OPTIONS.map((option) => {
const Icon = option.icon
return (
<DropdownMenuItem
key={option.type}
active={column.type === option.type}
onSelect={() => onChangeType(column.key, option.type)}
>
<Icon />
{option.label}
</DropdownMenuItem>
)
})}
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
{onConfigure && typeDefinition.hasConfiguration && (
<DropdownMenuItem onSelect={() => onConfigure(column.key)}>
<Settings />
{`Configure ${typeDefinition.label.toLowerCase()}`}
</DropdownMenuItem>
)}
{onToggleUnique && (
<DropdownMenuItem onSelect={() => onToggleUnique(column.key)}>
<Fingerprint />
{column.unique ? 'Remove unique' : 'Set unique'}
</DropdownMenuItem>
)}
{onPinToggle && (
<DropdownMenuItem onSelect={() => onPinToggle(column.key)}>
{isPinned ? <PinOff /> : <Pin />}
Expand Down
Loading
Loading