Improve Table Editor design and accessibility (#50797)

## Problem

Controls in the new table sheet's Columns section had inconsistent
styling, alignment and focus states. Some icon-only actions were faint
or lacked useful accessible names and tooltips, while related controls
used different dropdown treatments.

<img width="1428" height="250" alt="image"
src="https://github.com/user-attachments/assets/9d1ad80d-2de0-4736-8ddb-0260e10ddacc"
/>

## Solution

- Align the column headers and fields on the same grid, and keep the
foreign key action inside the name field in every state.
- Give the drag, foreign key, suggestions, extra options and remove
actions consistent button styling, sizing, hover and focus states, with
accessible names and tooltips.
- Move masking into Extra options and include it in the options count.
Remove the separators around the former inline controls.
- Use the design system's ComboboxTrigger for column types and align the
Default value select's radius with it. Update the suggestions dropdown
icon.
- Tidy related field labels and update the affected table editor tests.

The underlying column values and database behaviour are unchanged.

| Original before | Original after |
| --- | --- |
| <img width="1498" height="392" alt="CleanShot 2026-09-24 at 13 33
40@2x"
src="https://github.com/user-attachments/assets/56559e72-f289-44bc-999f-c1f3158c8f65"
/> | <img width="1496" height="416" alt="CleanShot 2026-09-24 at 13 32
42@2x"
src="https://github.com/user-attachments/assets/d05340fc-a460-4b5c-aaff-41127f692592"
/> |

## Review instructions

- Open **Table Editor → New table** and inspect the Columns section.
Check that Name, Type and Default value headers align with their fields.
- Tab through the row actions. Check their focus states, accessible
names and tooltips, including the foreign key control with and without a
relation.
- Drag a column to reorder it, open Extra options and toggle masking.
Check the options count and that the other settings still work.
- Compare the closed Type and Default value dropdowns, including an enum
default value.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Updated the column editor layout and controls for naming,
sensitive-data settings, extra options, foreign-key editing, and column
removal, with clearer tooltips and more consistent interactions.
* Added accessible help tooltips for the Name and Default Value fields.
* Updated column-type and suggested-value controls, including clearer
guidance for choosing a suggested value.
  * Sensitive-data settings are now available in the extra-options menu.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
This commit is contained in:
Gildas GarciaandDanny White authored and GitHub committed 2026-09-24 12:37:30 +02:00
1 parent 0d3b73794b
commit ca28c945db
8 files changed
+371 -322

No files matched your search

@@ -371,7 +371,7 @@ export const ColumnEditor = ({
isReactForm={false}
layout="flex"
id="isArray"
label="Define as Array"
label="Define as array"
description="Allow column to be defined as variable-length multidimensional arrays"
>
<Checkbox
@@ -484,7 +484,7 @@ export const ColumnEditor = ({
isReactForm={false}
layout="flex"
id="isUnique"
label="Is Unique"
label="Is unique"
description="Enforce values in the column to be unique across rows"
>
<Switch
@@ -1,14 +1,5 @@
import { noop } from 'lodash'
import {
Calendar,
Check,
ChevronsUpDown,
ExternalLink,
Hash,
ListPlus,
ToggleRight,
Type,
} from 'lucide-react'
import { Calendar, Check, ExternalLink, Hash, ListPlus, ToggleRight, Type } from 'lucide-react'
import Link from 'next/link'
import { ReactNode, useId, useState } from 'react'
import {
@@ -17,6 +8,7 @@ import {
AlertTitle,
Button,
cn,
ComboboxTrigger,
Command,
CommandEmpty,
CommandGroup,
@@ -188,14 +180,15 @@ const ColumnType = ({
{showLabel && <Label className="text-foreground-light">Type</Label>}
<Popover modal open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant={error ? 'danger' : 'default'}
role="combobox"
size={'small'}
<ComboboxTrigger
size="small"
aria-expanded={open}
aria-controls={listboxId}
className={cn('w-full justify-between', !hasValue && 'text-foreground-lighter')}
iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
aria-invalid={Boolean(error)}
className={cn(
!hasValue && 'text-foreground-lighter',
error && 'border-destructive-400'
)}
title={displayValue}
>
{hasValue ? (
@@ -206,7 +199,7 @@ const ColumnType = ({
) : (
'Choose a column type...'
)}
</Button>
</ComboboxTrigger>
</PopoverTrigger>
<PopoverContent id={listboxId} className="w-[460px] p-0" side="bottom" align="center">
<Command>
@@ -6,7 +6,7 @@
// the component over to the UI library
import { noop } from 'lodash'
import { List } from 'lucide-react'
import { ChevronDown } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import {
DropdownMenu,
@@ -106,12 +106,13 @@ const InputWithSuggestions = ({
<ButtonTooltip
variant="text"
size="tiny"
className="px-1 mr-0.5"
className="w-6.5 px-0 mr-0.5"
aria-label={suggestionsTooltip || 'Show suggestions'}
tooltip={{
content: { text: suggestionsTooltip || 'Suggestions', side: 'bottom' },
content: { text: 'Choose a suggested value for this column', side: 'bottom' },
}}
>
<List strokeWidth={1.5} size={14} />
<ChevronDown className="text-foreground-lighter" strokeWidth={1.5} size={16} />
</ButtonTooltip>
</DropdownMenuTrigger>
@@ -1,6 +1,6 @@
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { Eye, EyeOff, GripVertical, Link, Plus, Settings, X } from 'lucide-react'
import { GripVertical, Link, Plus, Settings, X } from 'lucide-react'
import { useState } from 'react'
import {
Badge,
@@ -12,7 +12,9 @@ import {
CommandItem,
CommandList,
CommandSeparator,
Input,
InputGroup,
InputGroupAddon,
InputGroupInput,
Popover,
PopoverContent,
PopoverTrigger,
@@ -57,6 +59,7 @@ interface ColumnProps {
isNewRecord: boolean
hasForeignKeys: boolean
hasImportContent: boolean
gridTemplateColumns: string
shouldAutoFocusName?: boolean
onUpdateColumn: (changes: Partial<ColumnField>) => void
onRemoveColumn: () => void
@@ -70,6 +73,7 @@ export const Column = ({
isNewRecord = false,
hasForeignKeys = false,
hasImportContent = false,
gridTemplateColumns,
shouldAutoFocusName = false,
onUpdateColumn,
onRemoveColumn,
@@ -83,6 +87,7 @@ export const Column = ({
column.isIdentity ? 1 : 0,
column.isUnique ? 1 : 0,
column.isArray ? 1 : 0,
column.isSensitiveData ? 1 : 0,
].reduce((a, b) => a + b, 0)
const { data } = useForeignKeyConstraintsQuery({
@@ -107,6 +112,21 @@ export const Column = ({
const hasChangesInRelations = relations
.map((r) => getRelationStatus(r))
.some((x) => x !== undefined)
const activeRelations = relations.filter((relation) => !relation.toRemove)
const onToggleSensitiveData = () => {
const marker = '[SENSITIVE]'
const isSensitiveData = !column.isSensitiveData
let updatedComment = column.comment || ''
if (isSensitiveData && !updatedComment.includes(marker)) {
updatedComment = `${updatedComment} ${marker}`.trim()
} else if (!isSensitiveData) {
updatedComment = updatedComment.replace(marker, '').trim()
}
onUpdateColumn({ isSensitiveData, comment: updatedComment })
}
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition } =
useSortable({
@@ -119,183 +139,158 @@ export const Column = ({
}
return (
<div className="flex w-full items-center" ref={setNodeRef} style={style}>
<div className={`w-[5%] ${!isNewRecord ? 'hidden' : ''}`}>
<button
ref={setActivatorNodeRef}
{...attributes}
{...listeners}
tabIndex={0}
className="opacity-50 hover:opacity-100 disabled:hover:opacity-50 transition cursor-grab text-foreground"
type="button"
>
<GripVertical size={16} strokeWidth={1.5} />
</button>
</div>
<div className="w-[30%]">
<div className="flex w-[95%] items-center justify-between">
<div className="h-4 w-px bg-border" />
<Input
autoFocus={shouldAutoFocusName}
aria-label="Column name"
size="small"
value={column.name}
title={column.name}
disabled={hasImportContent}
placeholder="column_name"
className={cn(
'[&>div>div>div>input]:py-1.5 [&>div>div>div>input]:border-r-transparent [&>div>div>div>input]:rounded-r-none',
hasImportContent ? 'opacity-50' : ''
)}
onChange={(event) => onUpdateColumn({ name: event.target.value })}
/>
{relations.filter((r) => !r.toRemove).length === 0 ? (
<div className="flex items-center gap-x-1">
<div
className="grid w-full items-center gap-x-1"
ref={setNodeRef}
style={{ ...style, gridTemplateColumns }}
>
{isNewRecord && (
<div className="flex items-center justify-center">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="dashed"
className="rounded-l-none h-[30px] py-0 px-2"
onClick={() => onEditForeignKey()}
>
<Link size={12} />
</Button>
<div className="h-4 w-px bg-border" />
<Tooltip>
<TooltipTrigger asChild>
<button
tabIndex={0}
onClick={() => {
const SENSITIVE_DATA_MARKER = '[SENSITIVE]'
const isSensitive = !column.isSensitiveData
let updatedComment = column.comment || ''
if (isSensitive && !updatedComment.includes(SENSITIVE_DATA_MARKER)) {
updatedComment = updatedComment
? `${updatedComment} ${SENSITIVE_DATA_MARKER}`
: SENSITIVE_DATA_MARKER
} else if (!isSensitive) {
updatedComment = updatedComment.replace(SENSITIVE_DATA_MARKER, '').trim()
}
onUpdateColumn({ isSensitiveData: isSensitive, comment: updatedComment })
}}
className={cn(
'transition cursor-pointer p-1 hover:bg-surface-100 rounded',
column.isSensitiveData
? 'opacity-100 text-foreground'
: 'opacity-50 hover:opacity-100 text-foreground-light'
)}
type="button"
aria-label={
column.isSensitiveData ? 'Marked as sensitive' : 'Not marked as sensitive'
}
variant="text"
ref={setActivatorNodeRef}
{...attributes}
{...listeners}
tabIndex={0}
className="w-6.5 px-0 cursor-grab text-foreground"
icon={<GripVertical size={16} strokeWidth={2} />}
aria-label={`Move column ${column.name}`}
/>
</TooltipTrigger>
<TooltipContent side="bottom">Drag to reorder columns</TooltipContent>
</Tooltip>
</div>
)}
<div className="min-w-0">
<div className="flex w-[95%] items-center">
<InputGroup className="min-w-0 flex-1">
<InputGroupInput
autoFocus={shouldAutoFocusName}
aria-label="Column name"
size="small"
value={column.name}
title={column.name}
disabled={hasImportContent}
placeholder="column_name"
className={hasImportContent ? 'opacity-50' : undefined}
onChange={(event) => onUpdateColumn({ name: event.target.value })}
/>
<InputGroupAddon align="inline-end">
{activeRelations.length === 0 ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="text"
size="tiny"
className="w-6.5 px-0 mr-0.5"
onClick={() => onEditForeignKey()}
aria-label={`Add foreign key for ${column.name}`}
>
<Link size={14} strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Reference a column in another table</TooltipContent>
</Tooltip>
) : (
<Popover open={open} onOpenChange={setOpen} modal={false}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="text"
size="tiny"
className="w-6.5 px-0 mr-0.5"
aria-label={`Edit ${column.name} foreign keys`}
>
<Link size={14} strokeWidth={1.5} />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">
View and add foreign key relations
</TooltipContent>
</Tooltip>
<PopoverContent
className={cn('p-0', hasChangesInRelations ? 'w-96' : 'w-72')}
side="bottom"
align="center"
>
{column.isSensitiveData ? (
<EyeOff size={14} strokeWidth={1.5} />
) : (
<Eye size={14} strokeWidth={1.5} />
)}
</button>
</TooltipTrigger>
<div className="text-xs px-2 pt-2">
Involved in {activeRelations.length} foreign key
{activeRelations.length > 1 ? 's' : ''}
</div>
<Command>
<CommandList>
<CommandGroup>
{activeRelations.map((relation, idx) => {
const key = String(relation?.id ?? `${column.id}-relation-${idx}`)
<TooltipContent side="bottom">
{column.isSensitiveData
? 'Data is masked in grid display. Actual data unchanged in database.'
: 'Mark as sensitive to mask in grid display'}
</TooltipContent>
</Tooltip>
</div>
) : (
<Popover open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger asChild>
<Button className="rounded-l-none h-[30px] py-0 px-2">
<Link size={12} />
</Button>
</PopoverTrigger>
<PopoverContent
className={cn('p-0', hasChangesInRelations ? 'w-96' : 'w-72')}
side="bottom"
align="center"
>
<div className="text-xs px-2 pt-2">
Involved in {relations.length} foreign key{relations.length > 1 ? 's' : ''}
</div>
<Command>
<CommandList>
<CommandGroup>
{relations.map((relation, idx) => {
const key = String(relation?.id ?? `${column.id}-relation-${idx}`)
const status = getRelationStatus(relation)
if (status === 'REMOVE') return null
return (
<CommandItem
key={key}
value={key}
className="cursor-pointer w-full"
onSelect={() => onEditForeignKey(relation)}
onClick={() => onEditForeignKey(relation)}
>
{status === undefined ? (
<div className="w-full flex items-center justify-between truncate">
{relation.name}
</div>
) : (
<div className="flex items-center gap-x-2 truncate">
<Badge variant={status === 'ADD' ? 'success' : 'warning'}>
{status}
</Badge>
<p className="truncate">
{relation.name || (
<>
To{' '}
{relation.columns
.filter((c) => c.source === column.name)
.map((c) => {
return (
<code key={`${c.source}-${c.target}`}>
{relation.schema}.{relation.table}.{c.target}
</code>
)
})}
{relation.columns.length > 1 && (
return (
<CommandItem
key={key}
value={key}
className="cursor-pointer w-full"
onSelect={() => onEditForeignKey(relation)}
onClick={() => onEditForeignKey(relation)}
>
{status === undefined ? (
<div className="w-full flex items-center justify-between truncate">
{relation.name}
</div>
) : (
<div className="flex items-center gap-x-2 truncate">
<Badge variant={status === 'ADD' ? 'success' : 'warning'}>
{status}
</Badge>
<p className="truncate">
{relation.name || (
<>
and {relation.columns.length - 1} other column
{relation.columns.length > 2 ? 's' : ''}
To{' '}
{relation.columns
.filter((c) => c.source === column.name)
.map((c) => (
<code key={`${c.source}-${c.target}`}>
{relation.schema}.{relation.table}.{c.target}
</code>
))}
{relation.columns.length > 1 && (
<>
and {relation.columns.length - 1} other column
{relation.columns.length > 2 ? 's' : ''}
</>
)}
</>
)}
</>
)}
</p>
</div>
)}
</p>
</div>
)}
</CommandItem>
)
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<CommandItem
className="cursor-pointer w-full gap-x-2"
onSelect={() => onEditForeignKey()}
onClick={() => onEditForeignKey()}
>
<Plus size={14} strokeWidth={1.5} />
<p>Add foreign key relation</p>
</CommandItem>
)
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<CommandItem
className="cursor-pointer w-full gap-x-2"
onSelect={() => onEditForeignKey()}
onClick={() => onEditForeignKey()}
>
<Plus size={14} strokeWidth={1.5} />
<p>Add foreign key relation</p>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)}
</InputGroupAddon>
</InputGroup>
</div>
</div>
<div className="w-[25%]">
<div className="min-w-0">
<div className="w-[95%]">
<ColumnType
value={{ format: column.format, formatSchema: column.formatSchema }}
@@ -313,14 +308,14 @@ export const Column = ({
/>
</div>
</div>
<div className={`${isNewRecord ? 'w-[25%]' : 'w-[30%]'}`}>
<div className="min-w-0">
<div className="w-[95%]">
<ColumnDefaultValue
columnFields={column}
enumTypes={enumTypes}
showLabel={false}
size="small"
className={`rounded-sm lg:gap-0 ${
className={`lg:gap-0 ${
column.format.includes('int') && column.isIdentity ? 'opacity-50' : ''
}`}
data-testid={`${column.name}-default-value`}
@@ -329,7 +324,7 @@ export const Column = ({
/>
</div>
</div>
<div className="w-[10%]">
<div className="min-w-0">
<Checkbox
aria-label="Check to make this column a primary key"
checked={column.isPrimaryKey}
@@ -342,113 +337,142 @@ export const Column = ({
}}
/>
</div>
<div className={`${hasImportContent ? 'w-[10%]' : 'w-[0%]'}`} />
<div className="flex w-[5%] justify-end">
{(!column.isPrimaryKey || column.format.includes('int')) && (
<Popover>
<PopoverTrigger
data-testid={`${column.name}-extra-options`}
className="group flex items-center -space-x-1"
>
{settingsCount > 0 && (
<div className="rounded-full bg-foreground h-4 w-4 flex items-center justify-center text-xs text-background">
{settingsCount}
</div>
)}
<div className="text-foreground-light transition-colors group-hover:text-foreground">
<Settings size={16} strokeWidth={1} />
</div>
</PopoverTrigger>
<PopoverContent align="end" className="w-80 p-0">
<div className="flex items-center justify-center bg-surface-200 gap-y-1 py-1.5 px-3 border-b border-overlay">
<h5 className="text-foreground">Extra options</h5>
</div>
{hasImportContent && <div />}
<div className="flex justify-end">
<Popover>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
data-testid={`${column.name}-extra-options`}
variant="outline"
aria-label={`Options for ${column.name}`}
className="w-6.5 px-0 relative"
icon={
<>
{settingsCount > 0 && (
<div className="absolute -top-1.5 -left-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-foreground text-[10px] font-medium leading-none tabular-nums text-background">
{settingsCount}
</div>
)}
<div className="text-foreground-light transition-colors group-hover:text-foreground">
<Settings size={16} strokeWidth={1.75} />
</div>
</>
}
/>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">
Set column constraints, array, and masking options
</TooltipContent>
</Tooltip>
<PopoverContent align="end" className="w-80 p-0">
<div className="flex items-center justify-center bg-surface-200 gap-y-1 py-1.5 px-3 border-b border-overlay">
<h5 className="text-foreground">Extra options</h5>
</div>
<div className="flex flex-col gap-y-4 p-4" key={`${column.id}_configuration`}>
{!column.isPrimaryKey && (
<FormItemLayout
isReactForm={false}
layout="flex"
<div className="flex flex-col gap-y-4 p-4" key={`${column.id}_configuration`}>
{!column.isPrimaryKey && (
<FormItemLayout
isReactForm={false}
layout="flex"
id="isNullable"
label="Is nullable"
description="Specify if the column can assume a NULL value if no value is provided"
>
<Checkbox
id="isNullable"
label="Is Nullable"
description="Specify if the column can assume a NULL value if no value is provided"
>
<Checkbox
id="isNullable"
checked={column.isNullable}
onCheckedChange={() => onUpdateColumn({ isNullable: !column.isNullable })}
/>
</FormItemLayout>
)}
{!column.isPrimaryKey && (
<FormItemLayout
isReactForm={false}
layout="flex"
checked={column.isNullable}
onCheckedChange={() => onUpdateColumn({ isNullable: !column.isNullable })}
/>
</FormItemLayout>
)}
{!column.isPrimaryKey && (
<FormItemLayout
isReactForm={false}
layout="flex"
id="isUnique"
label="Is unique"
description="Enforce if values in the column should be unique across rows"
>
<Checkbox
id="isUnique"
label="Is Unique"
description="Enforce if values in the column should be unique across rows"
>
<Checkbox
id="isUnique"
checked={column.isUnique}
onCheckedChange={() => onUpdateColumn({ isUnique: !column.isUnique })}
/>
</FormItemLayout>
)}
{column.format.includes('int') && (
<FormItemLayout
isReactForm={false}
layout="flex"
checked={column.isUnique}
onCheckedChange={() => onUpdateColumn({ isUnique: !column.isUnique })}
/>
</FormItemLayout>
)}
{column.format.includes('int') && (
<FormItemLayout
isReactForm={false}
layout="flex"
id="isIdentity"
label="Is Identity"
description="Automatically assign a sequential unique number to the column"
>
<Checkbox
id="isIdentity"
label="Is Identity"
description="Automatically assign a sequential unique number to the column"
>
<Checkbox
id="isIdentity"
checked={column.isIdentity}
onCheckedChange={() => {
const isIdentity = !column.isIdentity
const isArray = isIdentity ? false : column.isArray
onUpdateColumn({ isIdentity, isArray })
}}
/>
</FormItemLayout>
)}
{!column.isPrimaryKey && (
<FormItemLayout
isReactForm={false}
layout="flex"
checked={column.isIdentity}
onCheckedChange={() => {
const isIdentity = !column.isIdentity
const isArray = isIdentity ? false : column.isArray
onUpdateColumn({ isIdentity, isArray })
}}
/>
</FormItemLayout>
)}
{!column.isPrimaryKey && (
<FormItemLayout
isReactForm={false}
layout="flex"
id="defineAsArray"
label="Define as array"
description="Define your column as a variable-length multidimensional array"
>
<Checkbox
id="defineAsArray"
label="Define as Array"
description="Define your column as a variable-length multidimensional array"
>
<Checkbox
id="defineAsArray"
checked={column.isArray}
onCheckedChange={() => {
const isArray = !column.isArray
const isIdentity = isArray ? false : column.isIdentity
onUpdateColumn({ isArray, isIdentity })
}}
/>
</FormItemLayout>
)}
</div>
</PopoverContent>
</Popover>
)}
checked={column.isArray}
onCheckedChange={() => {
const isArray = !column.isArray
const isIdentity = isArray ? false : column.isIdentity
onUpdateColumn({ isArray, isIdentity })
}}
/>
</FormItemLayout>
)}
<FormItemLayout
isReactForm={false}
layout="flex"
id={`${column.id}-isSensitiveData`}
label="Mark as sensitive"
description="Mask values in the grid display. Database values are unchanged."
>
<Checkbox
id={`${column.id}-isSensitiveData`}
checked={column.isSensitiveData ?? false}
onCheckedChange={onToggleSensitiveData}
/>
</FormItemLayout>
</div>
</PopoverContent>
</Popover>
</div>
{!hasImportContent && (
<div className="flex w-[5%] justify-end">
<button
type="button"
tabIndex={0}
aria-label="Remove column"
className="cursor-pointer"
onClick={() => onRemoveColumn()}
>
<X size={16} strokeWidth={1} />
</button>
<div className="flex justify-end">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
tabIndex={0}
aria-label={`Remove column ${column.name}`}
className="w-6.5 px-0 cursor-pointer"
onClick={() => onRemoveColumn()}
icon={<X size={16} strokeWidth={1.75} />}
/>
</TooltipTrigger>
<TooltipContent side="bottom">Remove this column from the table</TooltipContent>
</Tooltip>
</div>
)}
</div>
@@ -72,6 +72,18 @@ export const ColumnManagement = ({
const [columnIdToFocus, setColumnIdToFocus] = useState<string | null>(null)
const hasImportContent = !isEmpty(importContent)
const columnGridTemplate = [
isNewRecord && '26px',
'minmax(0, 30fr)',
'minmax(0, 25fr)',
`minmax(0, ${isNewRecord ? 25 : 30}fr)`,
'minmax(0, 10fr)',
hasImportContent && 'minmax(0, 10fr)',
'26px',
!hasImportContent && '26px',
]
.filter(Boolean)
.join(' ')
const [primaryKeyColumns, otherColumns] = partition(
columns,
(column: ColumnField) => column.isPrimaryKey
@@ -222,14 +234,24 @@ export const ColumnManagement = ({
<div className="space-y-2">
{/* Headers */}
<div className="flex w-full px-3">
<div
className={`grid items-center gap-x-1 w-full ${isNewRecord ? 'px-3' : ''}`}
style={{ gridTemplateColumns: columnGridTemplate }}
>
{/* Drag handle */}
{isNewRecord && <div className="w-[5%]" />}
<div className="w-[25%] flex items-center space-x-2">
{isNewRecord && <div />}
<div className="min-w-0 flex items-center space-x-2">
<h5 className="text-xs text-foreground-lighter">Name</h5>
<Tooltip>
<TooltipTrigger>
<HelpCircle size={15} strokeWidth={1.5} className="text-foreground-lighter" />
<TooltipTrigger asChild>
<Button
variant="text"
aria-label="Show help for column Name"
className="w-6.5 px-0"
icon={
<HelpCircle size={15} strokeWidth={1.5} className="text-foreground-lighter" />
}
/>
</TooltipTrigger>
<TooltipContent side="bottom" className="w-[300px]">
Recommended to use lowercase and use an underscore to separate words e.g.
@@ -237,14 +259,21 @@ export const ColumnManagement = ({
</TooltipContent>
</Tooltip>
</div>
<div className="w-[25%]">
<div className="min-w-0">
<h5 className="text-xs text-foreground-lighter">Type</h5>
</div>
<div className={`${isNewRecord ? 'w-[25%]' : 'w-[30%]'} flex items-center space-x-2`}>
<h5 className="text-xs text-foreground-lighter">Default Value</h5>
<div className="min-w-0 flex items-center space-x-2">
<h5 className="text-xs text-foreground-lighter">Default value</h5>
<Tooltip>
<TooltipTrigger>
<HelpCircle size={15} strokeWidth={1.5} className="text-foreground-lighter" />
<TooltipTrigger asChild>
<Button
variant="text"
aria-label="Show help for column Default value"
className="w-6.5 px-0"
icon={
<HelpCircle size={15} strokeWidth={1.5} className="text-foreground-lighter" />
}
/>
</TooltipTrigger>
<TooltipContent side="bottom" className="w-[300px]">
Can either be a literal or an expression. When using an expression wrap your
@@ -252,15 +281,15 @@ export const ColumnManagement = ({
</TooltipContent>
</Tooltip>
</div>
<div className="w-[10%]">
<div className="min-w-0">
<h5 className="text-xs text-foreground-lighter">Primary</h5>
</div>
{/* Empty space */}
<div className={`${hasImportContent ? 'w-[10%]' : 'w-0'}`} />
{hasImportContent && <div />}
{/* More config button */}
<div className="w-[5%]" />
<div />
{/* Delete button */}
{!hasImportContent && <div className="w-[5%]" />}
{!hasImportContent && <div />}
</div>
{primaryKeyColumns.length > 0 && (
@@ -286,6 +315,7 @@ export const ColumnManagement = ({
hasForeignKeys={checkIfHaveForeignKeys(column)}
isNewRecord={isNewRecord}
hasImportContent={hasImportContent}
gridTemplateColumns={columnGridTemplate}
shouldAutoFocusName={column.id === columnIdToFocus}
onUpdateColumn={(changes) => onUpdateColumn(column, changes)}
onRemoveColumn={() => onRemoveColumn(column)}
@@ -318,6 +348,7 @@ export const ColumnManagement = ({
isNewRecord={isNewRecord}
hasForeignKeys={checkIfHaveForeignKeys(column)}
hasImportContent={hasImportContent}
gridTemplateColumns={columnGridTemplate}
shouldAutoFocusName={column.id === columnIdToFocus}
onUpdateColumn={(changes) => onUpdateColumn(column, changes)}
onRemoveColumn={() => onRemoveColumn(column)}
@@ -40,11 +40,11 @@ We've improved the column Type field so that it supports your [custom types](htt
![Column types](/images/blog/2021-sept/columns.png)
### Is Unique
### Is unique
We've made it simple to add a unique constraint when creating or editing a table.
![Is Unique](/images/blog/2021-sept/is-unique.png)
![Is unique](/images/blog/2021-sept/is-unique.png)
### Edit columns
@@ -58,8 +58,8 @@ test.describe('column editor complex types', () => {
'Type dropdown should show the bare base type, not the raw _int4 pg format'
).toBeVisible()
await expect(
columnEditor.getByRole('checkbox', { name: 'Define as Array' }),
'Define as Array should be checked for an existing array column'
columnEditor.getByRole('checkbox', { name: 'Define as array' }),
'Define as array should be checked for an existing array column'
).toBeChecked()
// Make an unrelated change and save - this must not corrupt the column's type.
+8 -8
View File
@@ -263,10 +263,10 @@ testRunner('table editor', () => {
await page.getByRole('button', { name: 'New table', exact: true }).click()
await page.getByTestId('table-name-input').fill(tableNameEnum)
await page.getByTestId('created_at-extra-options').click()
await page.getByText('Is Nullable').click()
await page.getByText('Is nullable').click()
await page.getByTestId('created_at-extra-options').click()
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(2).fill(columnNameEnum)
await page.getByLabel('Column name').nth(3).fill(columnNameEnum)
await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click()
await page.getByPlaceholder('Search types...').fill(enum_name)
// wait for response, then click
@@ -357,7 +357,7 @@ testRunner('table editor', () => {
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
await page.getByTestId('table-name-input').fill(tableNameUpdated)
await page.getByLabel('Column name').nth(2).fill(columnNameUpdated)
await page.getByLabel('Column name').nth(3).fill(columnNameUpdated)
const updateTablePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=column-update', {
method: 'POST',
})
@@ -948,7 +948,7 @@ testRunner('table editor', () => {
// Add boolean column
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(3).fill(boolColName)
await page.getByLabel('Column name').nth(4).fill(boolColName)
await page.getByText('Choose a column type...').click()
await page.getByPlaceholder('Search types...').fill('bool')
await page.getByRole('option', { name: 'bool' }).first().click()
@@ -1064,7 +1064,7 @@ testRunner('table editor', () => {
// Add nullable boolean column
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(3).fill(boolColName)
await page.getByLabel('Column name').nth(4).fill(boolColName)
await page.getByText('Choose a column type...').click()
await page.getByPlaceholder('Search types...').fill('bool')
await page.getByRole('option', { name: 'bool' }).first().click()
@@ -1985,14 +1985,14 @@ testRunner('table editor', () => {
await page.getByRole('button', { name: 'New table' }).click()
await page.getByLabel('Name', { exact: true }).fill(tableName)
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(2).fill('pw_column')
await page.getByLabel('Column name').nth(3).fill('pw_column')
await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click()
await page.getByRole('option').filter({ hasText: 'int8' }).click()
await page.getByLabel('Column default value').nth(2).fill('invalid')
await page.getByLabel('Column default value').nth(3).fill('invalid')
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText('invalid input syntax')).toBeVisible()
await page.getByLabel('Column default value').nth(2).fill('10')
await page.getByLabel('Column default value').nth(3).fill('10')
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText(`Table ${tableName} is good to go!`)).toBeVisible()
await expect(page.getByRole('button', { name: `View ${tableName}`, exact: true })).toBeVisible()