import { useState, type ReactNode } from 'react'; import { slugifyTableName, type TableMeta } from '@wellformed/shared'; import { api } from '../api/client'; import { useI18n } from '../i18n'; import { useWorkspace } from '../state/workspace'; import { useToasts } from '../state/toasts'; import { Modal } from './Modal'; /** * Making a table by hand, or changing what an existing one is called. The * demo no longer lives here: it is a property of workspace creation, chosen * in the picker, because a demo can only ever be the first thing in a * workspace. */ export function TableDialog({ table, onClose, }: { /** Given, the dialog edits that table instead of creating one. */ table?: TableMeta; onClose(): void; }): ReactNode { const ws = useWorkspace(); const toasts = useToasts(); const { t } = useI18n(); const editing = table !== undefined; const [label, setLabel] = useState(table?.label ?? ''); // Creating, this is the row-label column's *label* — its identifier is // derived. Editing, it is the identifier of the chosen existing column. const [display, setDisplay] = useState(table?.display ?? t('table.rowLabelDefault')); const [group, setGroup] = useState(table?.group ?? ''); const [saving, setSaving] = useState(false); // A link column cannot label its own rows; the server says so too. const labelColumns = (table?.columns ?? []).filter((c) => c.type !== 'link'); // The headings already in use, so a table joins a shelf by picking it rather // than by typing it letter-perfect a second time. const knownGroups = [ ...new Set((ws.meta?.tables ?? []).map((t) => t.group).filter((g): g is string => g !== null)), ]; // Shown, not chosen: people name the table in their own words and the // identifier is derived — the server steps it past collisions. What is // displayed here is the derivation, so the SQL bar never surprises anyone. const derivedName = editing ? table.name : slugifyTableName(label.trim()); const displayOk = display.trim().length > 0 && display.trim().length <= 60; const canSave = !saving && label.trim().length > 0 && displayOk; async function save() { setSaving(true); try { const groupValue = group.trim() || null; if (editing) { // Only what changed: setting the display column writes its own audit // line, and a no-op rename would leave a misleading one behind. await api.updateTable(table.name, { ...(label.trim() === table.label ? {} : { label: label.trim() }), ...(display.trim() === table.display ? {} : { display: display.trim() }), ...(groupValue === table.group ? {} : { group: groupValue }), }); await ws.refresh(); toasts.push(t('table.saved', label.trim() || table.label), 'success'); } else { const { table: created } = await api.createTable({ label: label.trim(), displayLabel: display.trim(), ...(groupValue ? { group: groupValue } : {}), }); await ws.refresh(); ws.setTable(created.name); toasts.push(t('table.created', created.label), 'success'); } onClose(); } catch (error) { toasts.fail(error); } finally { setSaving(false); } } return ( } >
{!editing && (
{t('table.runsBefore')} CREATE TABLE {derivedName || '…'} {t('table.runsAfter')}
)}
); }