import { useMemo, useState, type ReactNode } from 'react'; import { STORED_COLUMN_TYPES, type ColumnMeta, type Reading, type StoredColumnType, type Tightening, } 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'; import { TightenPanel } from './TightenPanel'; /** * Tightening one column, in its own room. * * This used to live inside "Edit column", which was the wrong place: renaming * a column and changing what its rows are allowed to hold are different acts * with different weights, and burying the second under the first is how it * went unfound. So the edit dialog keeps the decoration — name, format, * numbering — and this dialog holds the constraints: the type, a select's * options, a link's target, required, unique, each proposal checked against * every row by the dry run before Save is allowed to mean anything. */ interface TightenDialogProps { column: ColumnMeta; onClose(): void; } export function TightenDialog({ column, onClose }: TightenDialogProps): ReactNode { const ws = useWorkspace(); const toasts = useToasts(); const { t, locale } = useI18n(); const typeLabels: Record = { text: t('column.typeText'), number: t('column.typeNumber'), currency: t('column.typeCurrency'), date: t('column.typeDate'), checkbox: t('column.typeCheckbox'), select: t('column.typeSelect'), link: t('column.typeLink'), }; const [type, setType] = useState(column.type as StoredColumnType); const [options, setOptions] = useState((column.options ?? []).join(', ')); const [target, setTarget] = useState( column.target ?? ws.tables.find((tb) => tb.name !== ws.tableName)?.name ?? '', ); const [required, setRequired] = useState(column.required ?? false); const [unique, setUnique] = useState(column.unique ?? false); const [saving, setSaving] = useState(false); // How the text already in the column should be read while it converts, and // what to do with what will not. Both are answers the check asks for. const [reading, setReading] = useState({}); const [clear, setClear] = useState(false); const [verdict, setVerdict] = useState({ ready: true, clearable: 0 }); /** Only what actually changed; absent means "leave it as it is". */ const proposal = useMemo(() => { const next: Tightening = {}; if (type !== column.type) next.type = type; if (type === 'select') { const list = splitOptions(options); if (!sameList(list, column.options ?? [])) next.options = list; } if (type === 'link' && target !== column.target) next.target = target; if (required !== (column.required ?? false)) next.required = required; if (unique !== (column.unique ?? false)) next.unique = unique; return Object.keys(next).length ? next : null; }, [column, type, options, target, required, unique]); /** What the check needs to know, including the parts it cannot infer alone. */ const asked = useMemo( () => ({ ...(proposal ?? {}), // Sent even when unchanged: the answer to "does this still fit" depends // on the whole shape of the column, not only on the part being changed. type, ...(type === 'select' ? { options: splitOptions(options) } : {}), ...(type === 'link' ? { target } : {}), }), [proposal, type, options, target], ); const canSave = !saving && proposal !== null && (type !== 'select' || options.split(',').some((o) => o.trim())) && (type !== 'link' || Boolean(target)) && verdict.ready; async function save() { setSaving(true); try { const result = await api.tighten(ws.tableName, column.name, { ...asked, reading, onProblem: clear ? 'clear' : 'stop', }); if (result.cleared > 0) { toasts.push(t('column.tightened', column.label, result.cleared, locale), 'success'); } else { toasts.push(t('column.updated', column.label), 'success'); } await ws.refresh(); onClose(); } catch (error) { toasts.fail(error); } finally { setSaving(false); } } return ( } >
{type === 'select' && ( )} {type === 'link' && ( )} { setType('select'); setOptions(list.join(', ')); }} onVerdict={setVerdict} />
); } function splitOptions(raw: string): string[] { return raw .split(',') .map((s) => s.trim()) .filter(Boolean); } function sameList(a: string[], b: string[]): boolean { return a.length === b.length && a.every((v, i) => v === b[i]); }