Skip to content

Commit

Permalink
Rework universal schema editor (#205)
Browse files Browse the repository at this point in the history
* initial draft for implement universal schema editor

* add better type suggestion

* add mysql type suggestion
  • Loading branch information
invisal authored Dec 14, 2024
1 parent 99e3161 commit 1de02f3
Show file tree
Hide file tree
Showing 10 changed files with 696 additions and 29 deletions.
18 changes: 18 additions & 0 deletions src/app/storybook/column-type/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";
import ColumnTypeSelector from "@/components/gui/schema-editor/column-type-selector";
import { MYSQL_DATA_TYPE_SUGGESTION } from "@/drivers/mysql/mysql-data-type";
import { useState } from "react";

export default function ColumnTypeStorybook() {
const [value, setValue] = useState("");

return (
<div className="p-4">
<ColumnTypeSelector
value={value}
onChange={setValue}
suggestions={MYSQL_DATA_TYPE_SUGGESTION.typeSuggestions!}
/>
</div>
);
}
7 changes: 7 additions & 0 deletions src/app/storybook/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default async function StorybookRootLayout({
children,
}: {
children: React.ReactNode;
}) {
return <body>{children}</body>;
}
193 changes: 193 additions & 0 deletions src/components/gui/schema-editor/column-type-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ColumnTypeSuggestionGroup } from "@/drivers/base-driver";
import { useMemo, useState } from "react";

function ColumnTypeList({
items,
onChange,
}: {
items: ColumnTypeSuggestionGroup[];
onChange: (text: string) => void;
}) {
return items.map((group) => (
<div key={group.name}>
<div className="text-sm font-bold py-1 px-4 bg-muted mb-1">
{group.name}
</div>
<div className="flex flex-col">
{group.suggestions.map((type) => {
const itemClassName =
"py-0.5 px-3 pl-8 cursor-pointer hover:bg-gray-100";

const parameters = type.parameters ?? [];
let content = <span>{type.name.toUpperCase()}</span>;

if (parameters.length > 0) {
content = (
<>
{type.name.toUpperCase()}
<strong className="ml-0.5">(</strong>
<span className="text-muted-foreground">
{parameters.map((p) => p.name).join(", ")}
</span>
<strong>)</strong>
</>
);
}

return (
<div
key={type.name}
className={itemClassName}
onPointerDown={(e) => {
e.preventDefault();
}}
onClick={() => {
if (parameters.length > 0) {
onChange(
`${type.name.toUpperCase()}(${parameters.map((p) => p.default).join(",")})`
);
} else {
onChange(`${type.name.toUpperCase()}`);
}
}}
>
{content}
</div>
);
})}
</div>
</div>
));
}

export default function ColumnTypeSelector({
value,
onChange,
suggestions,
}: {
value: string;
onChange: (type: string) => void;
disabled?: boolean;
suggestions: ColumnTypeSuggestionGroup[];
}) {
const [showSuggestion, setShowSuggestion] = useState(false);

// Parse the value into type and parameters
const { parsedType, parsedParameters } = useMemo(() => {
const match = value.match(/([a-zA-Z]+)(\((.*)\))?/);

if (!match) {
return {
parsedType: "",
};
}

const type = match[1];
const parameters = match[3]?.split(",").map((p) => p.trim());

return { parsedType: type ?? "", parsedParameters: parameters };
}, [value]);

const filteredSuggestions = suggestions
.map((group) => {
{
return {
...group,
suggestions: group.suggestions.filter((type) => {
return type.name.toLowerCase().startsWith(parsedType.toLowerCase());
}),
};
}
})
.filter((group) => group.suggestions.length > 0);

let suggestionDom = (
<ColumnTypeList items={filteredSuggestions} onChange={onChange} />
);

// If there is only one suggestion left
if (
filteredSuggestions.length === 1 &&
filteredSuggestions[0].suggestions.length === 1 &&
filteredSuggestions[0].suggestions[0].name.toLowerCase() ===
parsedType.toLowerCase()
) {
const typeSuggestion = filteredSuggestions[0].suggestions[0];

suggestionDom = (
<div className="p-4">
<div className="text-lg font-semibold">
{typeSuggestion.name.toUpperCase()}
{typeSuggestion.parameters &&
typeSuggestion.parameters.length > 0 && (
<>
<strong>(</strong>
<span className="text-muted-foreground">
{typeSuggestion?.parameters.map((p) => p.name).join(", ")}
</span>
<strong>)</strong>
</>
)}
</div>

{typeof typeSuggestion.description === "string" && (
<div className="text-sm my-1 font-sans">
{typeSuggestion.description}
</div>
)}
{typeof typeSuggestion.description === "function" && (
<div
className="text-sm my-1 font-sans"
dangerouslySetInnerHTML={{
__html: typeSuggestion.description(parsedType, parsedParameters),
}}
/>
)}

{typeSuggestion.parameters && (
<ul className="flex flex-col gap-1 my-2 text-sm">
{typeSuggestion.parameters.map((p) => (
<li key={p.name}>
<strong className="font-semibold">{p.name}</strong>
<p className="text-muted-foreground">{p.description}</p>
</li>
))}
</ul>
)}
</div>
);
}

return (
<div className="relative">
<input
className="p-2 text-sm outline-none w-[150px] bg-inherit"
onFocus={() => setShowSuggestion(true)}
onBlur={() => {
setShowSuggestion(false);
console.log("blur");
}}
value={value}
onChange={(e) => {
onChange(e.target.value);
}}
/>
{showSuggestion && (
<Popover open={showSuggestion} modal={false}>
<PopoverTrigger />
<PopoverContent
className="w-[300px] max-h-[300px] p-0 overflow-y-auto mt-2 font-mono"
onOpenAutoFocus={(e) => e.preventDefault()}
>
{suggestionDom}
</PopoverContent>
</Popover>
)}
</div>
);
}
69 changes: 46 additions & 23 deletions src/components/gui/schema-editor/schema-editor-column-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
SelectValue,
} from "../../ui/select";
import { CSS } from "@dnd-kit/utilities";
import { convertSqliteType } from "@/drivers/sqlite/sql-helper";
import { Checkbox } from "@/components/ui/checkbox";
import ColumnDefaultValueInput from "./column-default-value-input";
import { checkSchemaColumnChange } from "@/components/lib/sql-generate.schema";
Expand All @@ -25,7 +24,6 @@ import {
DatabaseTableColumnChange,
DatabaseTableColumnConstraint,
DatabaseTableSchemaChange,
TableColumnDataType,
} from "@/drivers/base-driver";
import { cn } from "@/lib/utils";
import ColumnPrimaryKeyPopup from "./column-pk-popup";
Expand All @@ -42,6 +40,8 @@ import {
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { useDatabaseDriver } from "@/context/driver-provider";
import ColumnTypeSelector from "./column-type-selector";

export type ColumnChangeEvent = (
newValue: Partial<DatabaseTableColumn> | null
Expand Down Expand Up @@ -90,6 +90,46 @@ function changeColumnOnIndex(
});
}

function ColumnItemType({
value,
onChange,
disabled,
}: {
value: string;
onChange: (type: string) => void;
disabled?: boolean;
}) {
const { databaseDriver } = useDatabaseDriver();

if (
databaseDriver.columnTypeSelector.type === "dropdown" &&
databaseDriver.columnTypeSelector.dropdownOptions
) {
return (
<Select value={value} onValueChange={onChange} disabled={disabled}>
<SelectTrigger className="bg-inherit border-0 rounded-none shadow-none text-sm">
<SelectValue placeholder="Select datatype" />
</SelectTrigger>
<SelectContent>
{databaseDriver.columnTypeSelector.dropdownOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.text}
</SelectItem>
))}
</SelectContent>
</Select>
);
}

return (
<ColumnTypeSelector
onChange={onChange}
value={value}
suggestions={databaseDriver.columnTypeSelector.typeSuggestions ?? []}
/>
);
}

function ColumnItem({
value,
idx,
Expand Down Expand Up @@ -128,13 +168,6 @@ function ColumnItem({
const column = value.new || value.old;
if (!column) return null;

const normalizeType = convertSqliteType(column.type);
let type = "TEXT";

if (normalizeType === TableColumnDataType.INTEGER) type = "INTEGER";
if (normalizeType === TableColumnDataType.REAL) type = "REAL";
if (normalizeType === TableColumnDataType.BLOB) type = "BLOB";

let highlightClassName = "";
if (value.new === null) {
highlightClassName = "bg-red-400 dark:bg-red-800";
Expand Down Expand Up @@ -173,21 +206,11 @@ function ColumnItem({
/>
</td>
<td className="border">
<Select
value={type}
onValueChange={(newType) => change({ type: newType })}
<ColumnItemType
value={column.type}
onChange={(newType) => change({ type: newType })}
disabled={disabled}
>
<SelectTrigger className="bg-inherit border-0 rounded-none shadow-none text-sm">
<SelectValue placeholder="Select datatype" />
</SelectTrigger>
<SelectContent>
<SelectItem value="INTEGER">Integer</SelectItem>
<SelectItem value="REAL">Real</SelectItem>
<SelectItem value="TEXT">Text</SelectItem>
<SelectItem value="BLOB">Blob</SelectItem>
</SelectContent>
</Select>
/>
</td>
<td className="border">
<ColumnDefaultValueInput
Expand Down
20 changes: 20 additions & 0 deletions src/drivers/base-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,25 @@ export interface DatabaseTableOperationReslt {
record?: Record<string, DatabaseValue>;
}

export interface ColumnTypeSuggestionGroup {
name: string;
suggestions: ColumnTypeSuggestion[];
}
export interface ColumnTypeSuggestion {
name: string;
parameters?: {
name: string;
description?: string;
default: string;
}[];
description?: string | ((type: string, parameters?: string[]) => string);
}
export interface ColumnTypeSelector {
type: "dropdown" | "text";
dropdownOptions?: { value: string; text: string }[];
typeSuggestions?: ColumnTypeSuggestionGroup[];
}

export interface DriverFlags {
defaultSchema: string;
optionalSchema: boolean;
Expand Down Expand Up @@ -246,6 +265,7 @@ export abstract class BaseDriver {
// Flags
abstract getFlags(): DriverFlags;
abstract getCurrentSchema(): Promise<string | null>;
abstract columnTypeSelector: ColumnTypeSelector;

// Helper class
abstract escapeId(id: string): string;
Expand Down
Loading

0 comments on commit 1de02f3

Please sign in to comment.