Allow setting the name and INTEGER type for primary key columns in the Admin UI's table editor. #28

This commit is contained in:
Sebastian Jeltsch
2025-03-10 22:10:58 +01:00
parent efcdb2260a
commit 2c8e368df8
2 changed files with 194 additions and 20 deletions

View File

@@ -96,7 +96,7 @@ function columnTypeField(
return (
<SelectField
label={() => <L>Type</L>}
disabled={fk() !== undefined}
disabled={disabled || fk() !== undefined}
options={columnDataTypes}
value={field().state.value}
onChange={field().handleChange}
@@ -567,6 +567,143 @@ export function ColumnSubForm(props: {
);
}
export function PrimaryKeyColumnSubForm(props: {
form: FormApiT<Table>;
colIndex: number;
column: Column;
allTables: Table[];
disabled: boolean;
}): JSX.Element {
const disabled = () => props.disabled;
const [name, setName] = createWritableMemo(() => props.column.name);
const [expanded, setExpanded] = createWritableMemo(
() => props.column.name !== "id",
);
const [fk, setFk] = createSignal<string | undefined>();
const Header = () => (
<div class="flex items-center justify-between">
<h2>{name()}</h2>
<div class="flex items-center gap-2">
<Badge class="p-1">Primary Key</Badge>
<TbChevronDown
size={20}
style={{
"justify-self": "center",
"align-self": "center",
transition: "rotate",
rotate: expanded() ? "180deg" : "",
"transition-duration": "300ms",
"transition-timing-function": transitionTimingFunc,
}}
/>
</div>
</div>
);
return (
<Card>
<Collapsible
class="collapsible"
open={expanded()}
onOpenChange={setExpanded}
>
<CardHeader>
<Collapsible.Trigger class="collapsible__trigger">
<Header />
</Collapsible.Trigger>
</CardHeader>
<Collapsible.Content class="collapsible__content">
<CardContent>
<div class="flex flex-col gap-2 py-1">
{/* Column presets */}
{!disabled() && (
<div class="flex justify-between gap-1">
<Label>Presets</Label>
<div class="flex gap-1">
<For each={primaryKeyPresets}>
{([name, preset]) => (
<Badge
class="p-1"
onClick={() => {
const columns = [
...props.form.state.values.columns,
];
const column = columns[props.colIndex];
const v = preset(column.name);
column.data_type = v.data_type;
column.options = v.options;
props.form.setFieldValue("columns", columns);
}}
>
{name}
</Badge>
)}
</For>
</div>
</div>
)}
{/* Column name field */}
<props.form.Field
name={`columns[${props.colIndex}].name`}
defaultValue={name()}
validators={{
onChange: ({ value }: { value: string | undefined }) => {
setName(value ?? "<missing>");
return value ? undefined : "Column name missing";
},
}}
>
{buildTextFormField({
label: () => <L>Name</L>,
disabled: disabled(),
})}
</props.form.Field>
{/* Column type field */}
<props.form.Field
name={`columns[${props.colIndex}].data_type`}
children={columnTypeField(
/*disabled=*/ true,
fk,
props.allTables,
)}
/>
{/* Column options: pk, not null, ... */}
<props.form.Field
name={`columns[${props.colIndex}].options`}
children={(field) => {
return (
<ColumnOptionsFields
column={props.column}
value={field().state.value}
onChange={field().handleChange}
allTables={props.allTables}
disabled={true}
fk={fk()}
setFk={setFk}
/>
);
}}
/>
</div>
</CardContent>
</Collapsible.Content>
</Collapsible>
</Card>
);
}
function L(props: { children: JSX.Element }) {
return <div class="w-[100px]">{props.children}</div>;
}
@@ -578,6 +715,32 @@ type Preset = {
options: ColumnOption[];
};
export const primaryKeyPresets: [string, (colName: string) => Preset][] = [
[
"UUIDv7",
(colName: string) => {
return {
data_type: "Blob",
options: [
{ Unique: { is_primary: true } },
{ Check: `is_uuid_v7(${colName})` },
{ Default: "(uuid_v7())" },
"NotNull",
],
};
},
],
[
"INTEGER",
(_colName: string) => {
return {
data_type: "Integer",
options: [{ Unique: { is_primary: true } }, "NotNull"],
};
},
],
];
const presets: [string, (colName: string) => Preset][] = [
[
"Default",

View File

@@ -1,4 +1,4 @@
import { createMemo, createSignal, Index } from "solid-js";
import { createMemo, createSignal, Index, Switch, Match } from "solid-js";
import type { Accessor } from "solid-js";
import { createForm } from "@tanstack/solid-form";
@@ -21,7 +21,11 @@ import {
buildTextFormField,
} from "@/components/FormFields";
import { SheetContainer } from "@/components/SafeSheet";
import { ColumnSubForm } from "@/components/tables/CreateAlterColumnForm";
import {
PrimaryKeyColumnSubForm,
ColumnSubForm,
primaryKeyPresets,
} from "@/components/tables/CreateAlterColumnForm";
import { invalidateConfig } from "@/lib/config";
import type { Column } from "@bindings/Column";
@@ -96,14 +100,7 @@ export function CreateAlterTableForm(props: {
columns: [
{
name: "id",
data_type: "Blob",
// Column constraints: https://www.sqlite.org/syntax/column-constraint.html
options: [
{ Unique: { is_primary: true } },
{ Check: "is_uuid_v7(id)" },
{ Default: "(uuid_v7())" },
"NotNull",
],
...primaryKeyPresets[0][1]("id"),
},
newDefaultColumn(1),
] satisfies Column[],
@@ -151,7 +148,7 @@ export function CreateAlterTableForm(props: {
<form.Field
name="strict"
children={buildBoolFormField({ label: () => "STRICT typing" })}
children={buildBoolFormField({ label: () => "STRICT (type-safe)" })}
/>
{/* columns */}
@@ -162,14 +159,28 @@ export function CreateAlterTableForm(props: {
<div class="w-full">
<div class="flex flex-col gap-2">
<Index each={field().state.value}>
{(c: Accessor<Column>, i) => (
<ColumnSubForm
form={form}
colIndex={i}
column={c()}
allTables={props.allTables}
disabled={i === 0}
/>
{(c: Accessor<Column>, i: number) => (
<Switch>
<Match when={i === 0}>
<PrimaryKeyColumnSubForm
form={form}
colIndex={i}
column={c()}
allTables={props.allTables}
disabled={original() !== undefined}
/>
</Match>
<Match when={i !== 0}>
<ColumnSubForm
form={form}
colIndex={i}
column={c()}
allTables={props.allTables}
disabled={false}
/>
</Match>
</Switch>
)}
</Index>
</div>