mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-01-28 12:48:44 -06:00
Allow setting the name and INTEGER type for primary key columns in the Admin UI's table editor. #28
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user