/** * Form pieces of the Version Control / Build, Execution, Deployment / Tools * pages: bound checkboxes, "[x] Limit … to: [N] unit" rows, radio rows, * two-column grids, string lists and editable two-column tables. */ import { useId, useState, type KeyboardEvent, type ReactNode } from "react"; import clsx from "clsx"; import { Checkbox, IconButton, Input } from "@/ui"; import { NumberInput, Row, SearchLabel, Toggle } from "../components/Form"; import { useLabelMatch, useSetting } from "../components/context"; import { selectPage } from "../model/staging"; import styles from "./Workflow.module.css"; export function BoundToggle({ k, label, hint, disabled, }: { k: string; label: string; hint?: ReactNode; disabled?: boolean; }) { const [value, set] = useSetting(k); return ; } export function Indented({ children }: { children: ReactNode }) { return
{children}
; } /** IDEA's two-column checkbox grid. */ export function Grid({ left, right }: { left: ReactNode; right: ReactNode }) { return (
{left}
{right}
); } /** `[x] Limit history to: [1000] rows` — a checkbox enabling a number. */ export function ToggleNumber({ toggleKey, numberKey, label, unit, min, max, hint, }: { toggleKey: string; numberKey: string; label: string; unit?: string; min: number; max: number; hint?: ReactNode; }) { const [on, setOn] = useSetting(toggleKey); const [n, setN] = useSetting(numberKey); const matched = useLabelMatch(label); return (
: } /> {unit && {unit}} {hint && {hint}}
); } export interface RadioChoice { value: V; label: string; } /** `Label: (•) A ( ) B ( ) C` bound to a key. */ export function RadioRow({ k, label, choices, hint, }: { k: string; label: string; choices: readonly RadioChoice[]; hint?: ReactNode; }) { const [value, set] = useSetting(k); const name = useId(); return (
{choices.map((c) => ( ))}
); } /** Link-style button that jumps to another settings page. */ export function PageLink({ page, children }: { page: string; children: ReactNode }) { return ( ); } export interface TableColumn { key: string; title: string; placeholder?: string; mono?: boolean; } /** * Editable table with a +/−/edit toolbar (IDEA's Issue Navigation / * environment variables tables). Rows are plain records of strings; a row * whose first column is empty is dropped on commit. */ export function TableEditor({ rows, columns, onChange, empty, "aria-label": ariaLabel, }: { rows: readonly Record[]; columns: readonly TableColumn[]; onChange: (rows: Record[]) => void; empty: string; "aria-label": string; }) { const [selected, setSelected] = useState(null); const [editing, setEditing] = useState<{ index: number; row: Record } | null>(null); const first = columns[0]!.key; const commit = () => { if (!editing) return; const next = [...rows]; const row = Object.fromEntries(columns.map((c) => [c.key, (editing.row[c.key] ?? "").trim()])); if (!row[first]) { if (editing.index < rows.length) next.splice(editing.index, 1); } else if (editing.index >= rows.length) next.push(row); else next[editing.index] = row; onChange(next); setEditing(null); setSelected(row[first] ? Math.min(editing.index, next.length - 1) : null); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); commit(); } else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); setEditing(null); } }; const remove = () => { if (selected === null) return; onChange(rows.filter((_, i) => i !== selected)); setSelected(null); }; const shown = editing && editing.index >= rows.length ? [...rows, editing.row] : rows; return (
setEditing({ index: rows.length, row: Object.fromEntries(columns.map((c) => [c.key, ""])) })} /> selected !== null && setEditing({ index: selected, row: { ...rows[selected]! } })} />
{columns.map((c) => ( ))} {shown.length === 0 && ( )} {shown.map((row, i) => ( setSelected(i)} onDoubleClick={() => setEditing({ index: i, row: { ...row } })} > {columns.map((c, ci) => ( ))} ))}
{c.title}
{empty}
{editing?.index === i ? ( setEditing({ index: i, row: { ...editing.row, [c.key]: e.target.value } })} onKeyDown={onKey} onBlur={(e) => { // Moving between the cells of the edited row keeps editing. if (!e.currentTarget.closest("tr")?.contains(e.relatedTarget as Node | null)) commit(); }} /> ) : ( row[c.key] )}
); }